0% found this document useful (0 votes)
34 views13 pages

Node Microservice

Uploaded by

ANAGO Winceslas
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
34 views13 pages

Node Microservice

Uploaded by

ANAGO Winceslas
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

🎯 Objective

Build a production‑grade, event‑driven microservices platform using Node.js, Express, TypeScript,


MongoDB, and a message broker (NATS JetStream). Includes local Docker setup, K8s manifests, CI/CD,
observability, and a reusable service template.

🧩 Core Services (minimum set)


• api-gateway: HTTP entry, auth, request fan‑out, rate‑limit.
• auth: users, JWT issuance/rotation, RBAC.
• catalog: products, categories, attributes, search index pump.
• inventory: stock reservations, adjustments, re‑stock.
• cart: user carts; emits CartCheckedOut .
• order: orchestrates order lifecycle (saga choreography), status.
• payment: intents/capture/webhooks; emits PaymentSucceeded/Failed .
• shipping: delivery quotes, fulfillment, tracking.
• notification: email/SMS/push consuming business events.

Infra/Shared: - nats (JetStream), mongodb (replica set), redis (cache/queues), nginx (edge), otel-collector,
prometheus, grafana, loki (logs), tempo (traces).

🏗️ High‑Level Architecture

[ Client ]
|
[ NGINX Edge (TLS, rate-limit) ]
|
[ API Gateway (AuthZ, BFF) ] ────────────────► Direct calls where needed
| (idempotent reads)
└────► Publish commands/events to NATS ─────────────────────────┐

[auth] [catalog] [inventory] [order] [payment] [shipping] [notification]
│ │ │ │ │ │
└─ Mongo ┴─ Mongo ───┴─ Mongo ┴─ Mongo ┴─ Mongo ┴─ Mongo

Observability: OpenTelemetry SDK → otel-collector → (Prometheus/Grafana, Loki,


Tempo)

1
✅ Technology Choices & Rationale
• NATS JetStream: simple, ultra‑fast, supports at‑least‑once delivery, consumer groups, durable
streams. Great fit for Node.
• MongoDB Replica Set: document store per service (database‑per‑service). Use Change Streams for
read models where useful.
• Express + TS: familiar ergonomics; strict types via zod.
• Redis: caching, rate‑limit buckets, idempotency keys.
• OpenTelemetry: end‑to‑end tracing/metrics/log correlation.

🗂️ Monorepo Layout (pnpm workspaces)

repo/
package.json (workspaces)
pnpm-workspace.yaml
tsconfig.base.json
.github/workflows/
deploy/ (k8s + helm overlays)
docker/
services/
api-gateway/
auth/
catalog/
inventory/
cart/
order/
payment/
shipping/
notification/
packages/
shared-config/ (env, logger, http)
shared-messaging/ (NATS client, codecs, idempotency)
shared-types/ (zod schemas + TS types for events/DTOs)

📦 Workspace Root Files


package.json (excerpt)

{
"name": "platform",
"private": true,
"workspaces": ["services/*", "packages/*"],

2
"scripts": {
"build": "pnpm -r run build",
"dev": "pnpm -r --parallel run dev",
"lint": "pnpm -r run lint",
"test": "pnpm -r run test"
},
"devDependencies": {
"typescript": "^5.6.2",
"tsx": "^4.17.0"
}
}

tsconfig.base.json

{
"compilerOptions": {
"target": "ES2021",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": ".",
"resolveJsonModule": true
}
}

pnpm-workspace.yaml

packages:
- "services/*"
- "packages/*"

🧱 Shared Messaging Package (packages/shared-messaging)


NATS client + codecs + idempotency

// packages/shared-messaging/src/nats.ts
import { connect, StringCodec, JetStreamClient, NatsConnection } from 'nats';

3
export type NatsOptions = { url: string; name: string; user?: string; pass?:
string };
export const sc = StringCodec();

let nc: NatsConnection; let js: JetStreamClient;

export async function natsConnect(opts: NatsOptions) {


nc = await connect({ servers: opts.url, name: opts.name, user: opts.user,
pass: opts.pass });
js = nc.jetstream();
return { nc, js };
}

export function getNC() { if(!nc) throw new Error('NATS not connected'); return
nc; }
export function getJS() { if(!js) throw new Error('NATS JS not ready'); return
js; }

export async function publish(subject: string, data: unknown, headers:


Record<string,string> = {}) {
const msg = JSON.stringify(data);
await getJS().publish(subject, sc.encode(msg), { headers: toHdrs(headers) });
}

function toHdrs(h: Record<string,string>) { const { headers } =


require('nats'); const hd = headers(); for(const k in h) hd.set(k, h[k]);
return hd; }

Event Subjects (convention)

<domain>.<aggregate>.<event>
order.created
order.paid
order.cancelled
payment.succeeded
inventory.reserved
inventory.released
cart.checked_out

Event Schema (zod + TS)

// packages/shared-types/src/events.ts
import { z } from 'zod';

4
export const OrderCreated = z.object({
orderId: z.string().uuid(),
userId: z.string(),
items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive(),
price: z.number().nonnegative() })),
total: z.number().nonnegative(),
currency: z.string().length(3),
idempotencyKey: z.string().uuid().optional(),
createdAt: z.string()
});
export type OrderCreated = z.infer<typeof OrderCreated>;

🧪 Service Template (copy to any service)

services/example/ (rename)
src/
index.ts
env.ts
http.ts
mongo.ts
routes.ts
events/
subscribers.ts
publishers.ts
package.json
tsconfig.json
Dockerfile

package.json

{
"name": "example",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p .",
"start": "node dist/src/index.js"
},
"dependencies": {
"express": "^4.19.2",
"mongodb": "^6.8.0",
"zod": "^3.23.8",
"nats": "^2.27.0",

5
"pino": "^9.3.2",
"@platform/shared-messaging": "*",
"@platform/shared-types": "*"
},
"devDependencies": {"tsx": "^4.17.0", "typescript": "^5.6.2"}
}

env.ts

export const env = {


PORT: process.env.PORT ?? '3000',
MONGO_URI: process.env.MONGO_URI!,
NATS_URL: process.env.NATS_URL!,
SERVICE_NAME: process.env.SERVICE_NAME ?? 'example',
};

mongo.ts

import { MongoClient } from 'mongodb';


export const mongo = new MongoClient(process.env.MONGO_URI!);
export async function connectMongo(){ if(!mongo.topology) await
mongo.connect(); return mongo; }

http.ts

import express from 'express';


export function buildHttp(){
const app = express();
app.use(express.json());
app.get('/health', (_,res)=>res.json({ status:'ok' }));
return app;
}

index.ts

import { natsConnect } from '@platform/shared-messaging';


import { buildHttp } from './http';
import { connectMongo } from './mongo';
import { env } from './env';

(async () => {

6
await connectMongo();
await natsConnect({ url: env.NATS_URL, name: env.SERVICE_NAME });

const app = buildHttp();


const server = app.listen(env.PORT, () => console.log(`${env.SERVICE_NAME} on
${env.PORT}`));

const shutdown = async () => { server.close(()=>process.exit(0)); };


process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown);
})();

Dockerfile

FROM node:20-alpine as base


WORKDIR /app
COPY package*.json pnpm-lock.yaml* ./
RUN npm i -g pnpm && pnpm i --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=base /app/dist ./dist
COPY package*.json ./
RUN npm i -g pnpm && pnpm i --prod --frozen-lockfile
CMD ["node", "dist/src/index.js"]

🧪 Example: Order Placement Saga (Choreography)


1) cart emits cart.checked_out → 2) order creates Pending, emits order.created → 3) inventory
reserves stock, emits inventory.reserved or inventory.rejected → 4) payment processes charge,
emits payment.succeeded or payment.failed → 5) order transitions to Confirmed/Shipped/
Cancelled accordingly → 6) notification listens to order & payment events to message user.

Idempotency: include idempotencyKey on commands; consumers dedupe via Redis set with TTL.

🧪 Local Dev (Docker Compose)


docker/compose.dev.yml

7
version: "3.9"
services:
nats:
image: nats:2.10-alpine
command: ["-js", "-sd", "/data"]
ports: ["4222:4222", "8222:8222"]
volumes: ["nats-data:/data"]

mongo:
image: bitnami/mongodb:7.0
environment:
- MONGODB_REPLICA_SET_MODE=primary
- MONGODB_REPLICA_SET_NAME=rs0
- MONGODB_ROOT_PASSWORD=secret
- MONGODB_ADVERTISED_HOSTNAME=mongo
ports: ["27017:27017"]
volumes: ["mongo-data:/bitnami/mongodb"]

redis:
image: redis:7-alpine
ports: ["6379:6379"]

api-gateway:
build: ../services/api-gateway
environment:
- NATS_URL=nats://nats:4222
- MONGO_URI=mongodb://root:secret@mongo:27017/gateway?
authSource=admin&replicaSet=rs0
- SERVICE_NAME=api-gateway
depends_on: [nats, mongo]

auth:
build: ../services/auth
environment:
- NATS_URL=nats://nats:4222
- MONGO_URI=mongodb://root:secret@mongo:27017/auth?
authSource=admin&replicaSet=rs0
- SERVICE_NAME=auth
depends_on: [nats, mongo]

# ... repeat for catalog, inventory, cart, order, payment, shipping,


notification

volumes:
nats-data: {}
mongo-data: {}

8
Start:

pnpm i
docker compose -f docker/compose.dev.yml up -d --build
pnpm -C services/auth dev

🌐 Edge & Gateway


• NGINX (edge): TLS termination, IP allowlists/deny, request size/window rate limits, gzip/brotli.
• API Gateway: Express w/ JWT verification, per‑route RBAC, request idempotency via
Idempotency-Key header + Redis, fan‑out to services via HTTP or async to NATS.

Minimal NGINX snippet:

http {
map $http_x_forwarded_proto $proto { default $scheme; } # trust LB
server {
listen 80; return 301 https://$host$request_uri; }
server {
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/privkey.pem;

location /api/ { proxy_pass http://api-gateway:3000/; proxy_set_header X-


Request-Id $request_id; }
}
}

🔐 Security Baseline
• JWT short‑lived access + rotating refresh; JTI blacklist in Redis on rotation.
• mTLS optional inside cluster for payment webhooks.
• Input validation via zod on all controllers + event consumers.
• Least‑privilege MongoDB users per service; no cross‑db roles.
• Secrets via env (dev) and K8s Secret + sealed‑secrets (prod).
• Idempotency on publish/consume; exactly‑once is simulated via dedup and compensations.

9
📈 Observability
• Node SDK (OpenTelemetry) → otel-collector → Prometheus (metrics), Tempo (traces), Loki
(logs), Grafana (dashboards).

docker/otel-collector-config.yaml (minimal)

receivers:
otlp:
protocols: { http: {}, grpc: {} }
exporters:
prometheus: { endpoint: "0.0.0.0:9464" }
loki: { endpoint: http://loki:3100/loki/api/v1/push }
otlphttp/tempo: { endpoint: http://tempo:4318 }
processors:
batch: {}
ext:
headers: {}
service:
pipelines:
metrics: { receivers: [otlp], processors: [batch], exporters: [prometheus] }
logs: { receivers: [otlp], processors: [batch], exporters: [loki] }
traces: { receivers: [otlp], processors: [batch], exporters: [otlphttp/
tempo] }

Add to each service:

import * as otel from '@opentelemetry/sdk-node';


// init once per process; export OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT

☸️ Kubernetes (K8s) — Minimal Manifests


deploy/base/nats.yaml

apiVersion: apps/v1
kind: Deployment
metadata: { name: nats }
spec:
replicas: 1
selector: { matchLabels: { app: nats } }
template:
metadata: { labels: { app: nats } }

10
spec:
containers:
- name: nats
image: nats:2.10-alpine
args: ["-js", "-sd", "/data"]
ports: [{ containerPort: 4222 }, { containerPort: 8222 }]
volumeMounts: [{ name: data, mountPath: /data }]
volumes: [{ name: data, emptyDir: {} }]
---
apiVersion: v1
kind: Service
metadata: { name: nats }
spec:
selector: { app: nats }
ports:
- { name: client, port: 4222, targetPort: 4222 }
- { name: monitor, port: 8222, targetPort: 8222 }

deploy/base/service-template.yaml

apiVersion: apps/v1
kind: Deployment
metadata: { name: example }
spec:
replicas: 2
selector: { matchLabels: { app: example } }
template:
metadata: { labels: { app: example } }
spec:
containers:
- name: example
image: ghcr.io/your-org/example:{{TAG}}
env:
- { name: NATS_URL, value: nats://nats:4222 }
- { name: MONGO_URI, valueFrom: { secretKeyRef: { name: mongo, key:
uri } } }
- { name: SERVICE_NAME, value: example }
ports: [{ containerPort: 3000 }]
---
apiVersion: v1
kind: Service
metadata: { name: example }
spec:
selector: { app: example }
ports: [{ port: 3000, targetPort: 3000 }]

11
Ingress (if using ingress‑nginx):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-gateway
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
ingressClassName: nginx
tls: [{ hosts: [api.yourdomain.com], secretName: tls-cert }]
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: api-gateway, port: { number: 3000 } } }

🔄 CI/CD (GitHub Actions)


.github/workflows/ci.yml

name: ci
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- run: pnpm i --frozen-lockfile
- run: pnpm -r lint && pnpm -r test && pnpm -r build
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{
secrets.GITHUB_TOKEN }} }
- name: Build & push images
run: |
for svc in services/*; do
name=$(basename $svc)
docker build -t ghcr.io/${{ github.repository }}/$name:$
{{ github.sha }} $svc

12
docker push ghcr.io/${{ github.repository }}/$name:${{ github.sha }}
done
- name: K8s deploy
uses: azure/k8s-deploy@v5
with:
namespace: default
manifests: |
deploy/base/nats.yaml
deploy/base/service-template.yaml
images: |
ghcr.io/${{ github.repository }}/api-gateway:${{ github.sha }}

🧩 Developer Experience
• Makefile targets: make up , make down , make logs S=order , make seed .
• Hot‑reload with tsx watch and local Docker infra only.
• Contract tests using pactum or supertest + zod validation on samples.

📝 Next Steps (ready-to-run)


1. Initialize repo with this workspace + packages.
2. Copy Service Template into services/
{auth,catalog,inventory,cart,order,payment,shipping,notification} .
3. Wire minimal routes + event handlers per service using the event subjects.
4. docker compose -f docker/compose.dev.yml up -d to bring infra.
5. pnpm -C services/auth dev etc. to boot services.
6. Add OpenTelemetry SDK + exporter vars; bring up Grafana/Loki/Tempo stack when needed.

💡 Tips
• Keep events backward compatible; use additive changes and version your subjects if needed
( order.created.v2 ).
• Apply outbox pattern when persisting and emitting events together (Mongo transaction + outbox
collection + background publisher).
• Add retry & DLQ streams per domain with NATS JetStream consumer policies.
• Immutable event payloads; corrections via compensating events.

This blueprint is intentionally pragmatic: you can start small (auth, catalog, order, payment) and evolve
safely toward full event‑driven choreography with robust observability and CI/CD.

13

You might also like