BrilworksarrowBlogarrowTechnology Practices

How to Build Microservices with Nodejs

Hitesh Umaletiya
Hitesh Umaletiya
May 22, 2025
Clock icon6 mins read
Calendar iconLast updated May 28, 2025
Illustration of a person using a laptop with "Developing Microservices with Node.js [A Developer Guide]" text and Brilworks logo on a gradient background.

Microservices are a crucial part of modern software architecture, offering unparalleled scalability. If you're considering application modernization, chances are you'll adopt microservices, or your software consulting partner may have advised moving from a monolithic architecture to microservices as an application modernization strategy. Today, numerous technologies support microservices development, including Node.js, Java, Golang, Python, and Kotlin, making the process more accessible.

In this article, we'll explore how to build microservices with Node.js. Why is it a great choice for APIs, real-time applications, and lightweight and stateless services? 

Due to its lightweight nature, Node.js has become a favorite among professionals. We'll provide an example of how to build a microservice with Node.js. Of course, every technology comes with trade-offs, and Node.js is no exception. We'll also briefly go through some of the associated challenges.

Why Microservices and Node.js?

The microservices concept is breaking an application into small, loose-coupled, independently deployable services, each with a distinctive piece of functionality. It is the opposite of a monolithic architecture, which is a single code base to do everything and often produces complexity and slow iteration as the application matures.

Microservices Adoption In Software Development

Microservices allow teams to work on adjacent pieces of the system simultaneously, deploy pieces of their code without redeploying the entire application stack, and scale the different parts of a system according to their individual needs.

Node.js aligns very well to this philosophy. Node.js has an event-driven, non-blocking I/O architecture built upon the V8 JavaScript engine, making it a solid performer when processing asynchronous tasks. Hence, it is a great fit when microservices are I/O heavy, e.g., API calls, database queries, or stream data.

Node Js Event Driven Architecture

Its vibrant ecosystem, with npm offering a vast library of packages, lets developers quickly integrate tools for routing, logging, or authentication. Plus, JavaScript's ubiquity means teams can use the same language across frontend and backend, streamlining development.

But it's not all smooth sailing. Microservices may introduce complexity for distributed systems, inter-service communication, and data consistency. Node.js, while powerful, can trip you up if you're not mindful of its single-threaded nature or memory management. Let's explore how to navigate these challenges and build a robust microservices system.

Benefits Of Node Js In Microservice Development

Read more: How microservices enhances application development

A Practical Example

Assume you're building an e-commerce platform. Instead of a monolith, you decide to split it into microservices: one for user accounts, another for product inventory, a third for order processing, and a fourth for payment handling. Each service will be a standalone Node.js application, communicating via APIs.

This setup lets you scale the payment service during a flash sale without touching the user service, for example. To make this concrete, let's focus on the user service, which handles user registration, authentication, and profile management.

We'll build it with Node.js, using Express for routing and MongoDB for persistence, and touch on how it integrates with other services. The principles here apply to other services in the system.

See also: A guide to hiring Nodejs developers in 2025

1. Set up the User Service

First, you'll need a Node.js environment. Assuming you have Node.js and npm installed, create a new directory for the user service and initialize it:

{
"name": "user-service",
"version": "1.0.0",
"description": "Microservice for user management",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"express": "^4.17.1",
"mongoose": "^6.0.12",
"dotenv": "^10.0.0",
"jsonwebtoken": "^8.5.1",
"bcryptjs": "^2.4.3"
},
"devDependencies": {
"nodemon": "^2.0.14"
}
}

This package.json sets up the project with Express for the API, Mongoose for MongoDB interaction, jsonwebtoken for authentication, bcryptjs for password hashing, and nodemon for development. Install the dependencies with npm install.

Next, create the main application file, index.js, to set up the Express server and connect to MongoDB:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');
const app = express();
app.use(express.json());
const PORT = process.env.PORT || 3000;
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/user-service';
mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('MongoDB connection error:', err));
app.use('/api/users', userRoutes);
app.listen(PORT, () => {
console.log(`User service running on port ${PORT}`);
});

This code initializes an Express server, connects to MongoDB, and mounts user-related routes. The .env file (via dotenv) stores sensitive data like the MongoDB URI and port number.

2. Model the User

Define a Mongoose schema for users in models/User.js:

const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
name: { type: String, required: true },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', userSchema);

This schema defines a user with an email, password, name, and creation timestamp. The unique constraint on email prevents duplicate accounts.

3. Create the API

Now, create the routes in routes/userRoutes.js. This file handles user registration and authentication:

const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({ email, password: hashedPassword, name });
await user.save();
res.status(201).json({ message: 'User created', userId: user._id });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

The /register endpoint hashes the user's password and saves the user to MongoDB. The /login endpoint verifies credentials and issues a JSON Web Token (JWT) for authentication. Store the JWT_SECRET in your .env file.

4. Enable Inter-Service Communication

Microservices need to talk to each other. For example, the order service might need user details from the user service. REST APIs are a common choice, but for Node.js, you can also use message queues like RabbitMQ or Kafka for asynchronous communication. Let's implement a simple REST-based approach where the order service fetches user data.

Add an endpoint to userRoutes.js to retrieve user details by ID, protected by JWT:

const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const router = express.Router();
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
router.post('/register', async (req, res) => {
try {
const { email, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({ email, password: hashedPassword, name });
await user.save();
res.status(201).json({ message: 'User created', userId: user._id });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.get('/:id', authenticate, async (req, res) => {
try {
const user = await User.findById(req.params.id).select('-password');
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;

The authenticate middleware verifies the JWT, and the /users/:id endpoint returns user details (excluding the password) for authorized requests. The order service can call this endpoint with a valid token to fetch user data.

5. Deploy

Deploying a Node.js microservice involves containerization for consistency. Docker is a popular choice. Here's a simple Dockerfile for the user service:

FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Build and run the Docker image:
docker build -t user-service .
docker run -p 3000:3000 --env-file .env user-service

For orchestration, Kubernetes is a strong choice. Define a Kubernetes deployment in user-service-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
spec:
replicas: 2
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: user-service:latest
ports:
- containerPort: 3000
env:
- name: MONGODB_URI
valueFrom:
secretKeyRef:
name: user-service-secrets
key: mongodb-uri
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: user-service-secrets
key: jwt-secret

This deployment runs two replicas of the user service, pulling sensitive data from Kubernetes secrets. Apply it with kubectl apply -f user-service-deployment.yaml.

Tips for Monitoring and Observability

Microservices demand continuous monitoring. You can use tools like Prometheus for metrics and Grafana for visualization. For logging, there are popular libraries available like, like Winston, to use.

const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error'}),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
module.exports = logger;

Add Winston to your routes to log requests and errors, enhancing traceability across services.

Challenges With Node Js In Microservice Development

Microservices with Node.js aren't without pitfalls. Distributed systems can fail in unpredictable ways, such as network latency, service outages, or data inconsistencies. Implement circuit breakers (e.g., with opossum) to handle failures gracefully.

Use API gateways like Kong or Express Gateway to manage cross-service requests and authentication. Data management is another challenge. Every microservice needs to manage its database (tight coupling is a soft-degree of service coupling and may imply slight future issues of eventual consistency).

For example, if the order service needs user data, it can locally cache some of this data or use event sourcing to propagate updates with an event-driven model like Kafka.

Testing is hugely impactful. Do unit tests for every service with either Jest or Mocha, and integrate tests that confirm that services are calling each other correctly. You can mock an external dependency to limit the scope to the microservice. You can use Postman for end-to-end tests, or Newman, to test actual calls to the services.

Finally, leverage DevOps. Deploy with CI/CD pipelines using GitHub Actions or Jenkins/Hudson. You will want to monitor how nodes are using resources as Node.js is single threaded. Consequently, you can create bottlenecks with too many resources on CPU-heavy or asynchronous tasks. You may need to use worker threads or offload heavy computation more typically to another service.

Node.js microservices face issues: latency, service failures, data inconsistency.

  1. Use circuit breakers (e.g., Opossum) to handle failures.

  2. API gateways (Kong, Express Gateway) manage routing and auth.

  3. Each service should own its own database — watch for eventual consistency.

  4. Fix consistency: use local caching or event-driven updates (Kafka).

  5. Testing:

    1. Unit tests (Jest, Mocha)

    2. Integration tests with mocks

    3. End-to-end tests (Postman, Newman)

  6. CI/CD with GitHub Actions, Jenkins, or Hudson.

  7. Node.js is single-threaded; CPU-heavy tasks can choke it.

Use worker threads or offload compute to other services.

Conclusion

Building microservices with Node.js is a powerful way to create scalable, flexible systems. By leveraging Node's asynchronous strengths, Express for APIs, and tools like Docker and Kubernetes for deployment, you can craft a robust architecture.

The user service example shows how to structure a service, secure it, and prepare it for production. While challenges like distributed system complexity and data consistency arise, careful design and tooling can mitigate them. As you expand your system, keep services small, focused, and loosely coupled, and let Node.js's ecosystem empower your development. Need help building or scaling your architecture? Hire Node.js developers to bring expertise and speed to your microservices projects.

 

Hitesh Umaletiya

Hitesh Umaletiya

Co-founder of Brilworks. As technology futurists, we love helping startups turn their ideas into reality. Our expertise spans startups to SMEs, and we're dedicated to their success.

Get In Touch

Contact us for your software development requirements

You might also like

Get In Touch

Contact us for your software development requirements