Skip to content

S42-Core Microservice Example

Microservice users

This section explains how to set up a user microservice using s42-core.

Files

package.json

Defines the package metadata, dependencies, and scripts for the microservice. Key scripts include build, start, and test commands.

{
"name": "users-microservice",
"version": "1.0.0",
"description": "create users",
"main": "./dist/index.js",
"type": "module",
"scripts": {
"build": "rm -Rf ./dist && npx tsc --build",
"start:prod": "node --env-file=.production.env ./dist",
"start:local": "node --env-file=.local.env ./dist",
"dev": "npx tsx watch --env-file=.local.env ./src/index.ts",
"test": "npx tsx watch --env-file=.local.env ./src/test/index.ts"
},
"author": "César Casas",
"license": "ISC",
"dependencies": {
"s42-core": "1.0.0"
}
}

.local.env

Contains environment variables for local development, including ports and database URIs.

Terminal window
PORT=4530
MONGO_URI=mongodb://127.0.0.1:27017
MONGO_DB=s42
REDIS_URI=redis://127.0.0.1:6379
NODE_ENV=dev

tsconfig.json

Configures the TypeScript compiler options for the project, ensuring consistent and error-free compilation.

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"tsc": {
"swc": true,
"experimentalSpecifierResolution": "node"
}
}

src/index.ts

Sets up the microservice server, initializes necessary dependencies, and starts the server. Uses Cluster for process management, ensuring optimal resource utilization.

import { createServer } from 'node:http'
import {
Shutdown,
Cluster,
Dependencies,
MongoClient,
RedisClient,
EventsDomain,
RouteControllers,
} from 's42-core'
import { userController, healthController } from './controllers'
const port = process.env.PORT ?? 3000
Cluster(
0, // using all availables core
async (pid, uuid) => {
console.info('initializing: ', pid, uuid)
const mongoClient = MongoClient.getInstance({
connectionString: String(process.env?.MONGO_URI),
database: String(process.env?.MONGO_DB),
})
await mongoClient.connect()
const redisClient = RedisClient.getInstance(process.env.REDIS_URI)
const eventsDomain = EventsDomain.getInstance(redisClient, uuid)
Dependencies.add<MongoClient>('db', mongoClient)
Dependencies.add<RedisClient>('redis', redisClient)
Dependencies.add<EventsDomain>('eventsDomain', eventsDomain)
const routerControllers = RouteControllers.getInstance([
userController,
healthController,
])
const server = createServer(routerControllers.getCallback())
server.listen(port, () => {
console.info(`ready on *:${port}`)
})
Shutdown([mongoClient.close, redisClient.close, eventsDomain.close])
},
() => {
console.info('Error trying start servers')
},
)

src/controllers/index.ts

Exports controllers to be used in the microservice.

export { userController } from './users.js'
export { healthController } from './health.js'

src/controllers/health.ts

Defines a simple health check endpoint to verify that the server is running.

import { Controller } from 's42-core'
export const healthController = new Controller()
.get()
.setPath('/health')
.use(async (req, res) => {
res.end('Server working')
})

src/controllers/users.ts

Defines the user creation endpoint. Validates the request data, inserts a new user into the database, and emits a user creation event.

import { Dependencies, type MongoClient, type EventsDomain, Controller } from 's42-core'
import { z } from 'zod'
const TypeUser = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
})
export const userController = new Controller()
.setPath('/users/create')
.post()
.use(async (req, res, next) => {
console.info('This is a mws: ', req.query)
next()
})
.use(async (req, res) => {
const db = Dependencies.get<MongoClient>('db') as MongoClient
const eventsDomain = Dependencies.get<EventsDomain>('eventsDomain') as EventsDomain
try {
const data = req.body
TypeUser.parse(data)
await db.getCollection('users').insertOne({
...data,
remoteIp: req.realIp,
added: new Date(),
headers: req.headers,
})
eventsDomain.emitEvent('users.created', { ...data })
res.json({ ok: true })
} catch (error) {
res.jsonError({ ok: false, msg: error })
}
})

src/test/index.ts

Contains tests for the microservice, including emitting user creation events and verifying their handling

import { Test, EventsDomain, RedisClient } from 's42-core'
const redisInstance = RedisClient.getInstance(process.env.REDIS_URI)
const eventsDomain = EventsDomain.getInstance(redisInstance, 'testing-algo')
async function doEmitEventCreateUsers(eventX: number) {
try {
Test.Init('init doEmitEventCreateUsers')
eventsDomain.emitEvent(`users.created-${eventX}`, {
firstName: 'pepe',
lastName: 'luis',
email: 'cesarcasas@bsdsolutions.com.ar',
lang: 'es',
template: 'send-coupon',
})
Test.Ok('Test doEmitEventCreateUsers passed')
} catch (error) {
Test.Error('Test doInvalidTokenRequest failed:', error as Error)
}
}
async function runTests() {
for (let x = 0; x < 10000; x++) {
await doEmitEventCreateUsers(x)
}
Test.Finish()
}
console.info('Esperando instancias en listener')
const intervalId = setInterval(() => {
const events = Object.keys(eventsDomain.getAllRegisteredEvents())
if (events.length > 0) {
clearInterval(intervalId)
runTests()
}
}, 500)

Creating a Cell to Listen for User Creation Events and Send an Email

This section explains how to create a cell that listens for user creation events and sends a welcome email.

Files

.local.env

Contains environment variables for the cell, including ports and database URIs.

Terminal window
PORT=4545
REDIS_URI=redis://127.0.0.1:6379
NODE_ENV=dev

package.json

Defines the package metadata, dependencies, and scripts for the cell. Key scripts include build, start, and test commands.

{
"name": "cell-emails",
"version": "1.0.0",
"description": "Emails listener events",
"main": "./dist/index.js",
"type": "module",
"scripts": {
"build": "rm -Rf ./dist && npx tsc --build",
"start:prod": "node --env-file=.production.env ./dist",
"start:local": "node --env-file=.local.env ./dist",
"dev": "npx tsx watch --env-file=.local.env ./src/index.ts",
"test": "npx tsx watch --env-file=.local.env ./src/test/index.ts"
},
"author": "César Casas",
"license": "ISC",
"dependencies": {
"ioredis": "^5.4.1",
"s42-core": "*"
}
}

tsconfig.json

Configures the TypeScript compiler options for the cell, ensuring consistent and error-free compilation.

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"declaration": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"],
"tsc": {
"swc": true,
"experimentalSpecifierResolution": "node"
}
}

src/index.ts

Sets up the cell to listen for user creation events. When an event is received, it sends a welcome email and emits a confirmation event.

import { Shutdown, Cluster, EventsDomain, RedisClient } from 's42-core'
export type UsersCreated = {
email: string
firstName: string
lastName: string
lang: 'en' | 'es' | 'it' | 'fr'
template: string
}
Cluster(
1, // only one instance
async (pid, uuid) => {
console.info('initializing event user.created listener : ', pid, uuid)
const redisInstance = RedisClient.getInstance(process.env.REDIS_URI)
const eventsDomain = EventsDomain.getInstance(redisInstance, uuid)
eventsDomain.listenEvent<UsersCreated>(
`users.created`,
async (payload: UsersCreated) => {
try {
console.info('Email sent successfully:', payload)
eventsDomain.emitEvent('users.created.email.sent', { ok: true })
} catch (error) {
console.error('Error sending email:', error)
}
},
)
Shutdown([eventsDomain.close, redisInstance.close])
},
() => {
console.info('Error trying start servers')
},
)

test/index.ts

Contains tests for the cell, including emitting user creation events and verifying email sending.

import { Test, EventsDomain, RedisClient } from 's42-core'
const redisInstance = RedisClient.getInstance(process.env.REDIS_URI)
const eventsDomain = EventsDomain.getInstance(redisInstance, 'testing-algo')
async function doEmitEventCreateUsers() {
try {
Test.Init('init doEmitEventCreateUsers')
eventsDomain.emitEvent(`users.created`, {
firstName: 'pepe',
lastName: 'luis',
email: 'cesarcasas@bsdsolutions.com.ar',
lang: 'es',
template: 'send-coupon',
})
Test.Ok('Test doEmitEventCreateUsers passed')
} catch (error) {
Test.Error('Test doInvalidTokenRequest failed:', error as Error)
}
}
async function runTests() {
await doEmitEventCreateUsers()
Test.Finish()
}
console.info('waiting for listener instances')
const intervalId = setInterval(() => {
const events = Object.keys(eventsDomain.getAllRegisteredEvents())
if (events.length > 0) {
clearInterval(intervalId)
runTests()
}
}, 500)

Overview of the Process

  • Setting Up the Cell: Initialize EventsDomain and RedisClient, and subscribe to the users.created event.

  • Subscribing to Events: Use the listenEvent method to handle user creation events and send welcome emails.

  • Sending Emails: Implement the logic to send emails within the event handler.

  • Handling Shutdown: Use the Shutdown class to ensure the cell disconnects properly from the event system when stopping.

By following these steps, you can create a robust and scalable system that handles user creation and email sending efficiently using s42-core.

For assistance, you can contact me via my personal Telegram channel or by email: