A small microservices-based system built for the backend internship assignment: two backend services (User Service, Notification Service) that communicate only through NATS JetStream (no REST or WebSockets between them), fronted by an API Gateway.
See ARCHITECTURE.md for the full design rationale and
a diagram, and API_DOCS.md for the API reference.
microservices-assignment/
├── api-gateway/ # Public entry point — REST, JWT auth, forwarding
├── user-service/ # Registration/login, publishes events to NATS
├── notification-service/ # Subscribes to NATS events, sends notifications
├── docker-compose.yml # Runs the entire stack (NATS + 3 services)
├── ARCHITECTURE.md
├── API_DOCS.md
└── README.md
- Node.js 18+ (only needed if running services without Docker)
- Docker & Docker Compose (recommended — easiest way to run everything)
- In each service folder, copy the example env file:
cp user-service/.env.example user-service/.env cp notification-service/.env.example notification-service/.env cp api-gateway/.env.example api-gateway/.env
- Open each
.envfile and make sureJWT_SECRETandINTERNAL_API_KEYare identical across all three services (they must match — this is the shared secret gateway↔service auth described inARCHITECTURE.md). Set a real random value instead of the placeholder, e.g.:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" - From the project root:
docker compose up --build
- The API Gateway is now available at
http://localhost:4000.
You'll need a local NATS server with JetStream enabled:
# macOS
brew install nats-server
nats-server -js
# or via Docker, just for NATS itself
docker run -p 4222:4222 nats:2.10-alpine -jsThen, in three separate terminals:
cd user-service && cp .env.example .env && npm install && npm start
cd notification-service && cp .env.example .env && npm install && npm start
cd api-gateway && cp .env.example .env && npm install && npm start(Again — make sure JWT_SECRET and INTERNAL_API_KEY match across all
three .env files.)
# Register a user
curl -X POST http://localhost:4000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"name":"Jane Doe","email":"[email protected]","password":"password123"}'
# Log in
curl -X POST http://localhost:4000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"password123"}'
# -> copy the "token" from the response
# Get your profile
curl http://localhost:4000/api/users/me \
-H "Authorization: Bearer <token>"
# See notifications generated asynchronously via NATS
# (welcome email on register, login alert on login)
curl http://localhost:4000/api/notifications/me \
-H "Authorization: Bearer <token>"If everything is wired correctly, the notifications endpoint will show a
WELCOME_EMAIL and a LOGIN_ALERT entry — proof that the Notification
Service received events published by the User Service purely through NATS,
with no direct HTTP/WebSocket call between them.
| Requirement | How it's met |
|---|---|
| Two backend services + API Gateway | user-service/, notification-service/, api-gateway/ |
| No REST/WebSockets between the two services | They only communicate via NATS subjects (user.registered, user.login); neither knows the other's address |
| Message broker (NATS preferred) | NATS with JetStream for persistence |
| Secure communication | Internal shared-secret header (gateway↔service), JWT (client↔gateway), bcrypt password hashing, NATS user/pass auth support |
| Reliable, async delivery | JetStream durable consumer, at-least-once delivery, retry + redelivery on failure |
| Clean, scalable architecture | Stateless gateway, decoupled services, horizontal scalability (see ARCHITECTURE.md) |
| Error handling & validation | Input validators, try/catch everywhere, meaningful HTTP status codes |
| Secrets in environment variables | All secrets via .env / .env.example, nothing hardcoded |
- The "database" in this assignment is an in-memory Map for simplicity
and to keep the focus on the messaging architecture the assignment asks
about. In a production system this would be swapped for Postgres/Mongo
with minimal changes (only
user-service/server.js's storage calls would need to change). - "Sending" a notification currently just logs it and stores it in memory;
swapping in a real provider (SES, SendGrid, Twilio, etc.) would only
require changing the body of the two handler functions in
notification-service/server.js.