Building Scalable Microservices Architecture with Node.js and Express

Learn how to build a scalable microservices architecture using Node.js and Express with this beginner-friendly tutorial, including practical examples to get started quickly.

Microservices architecture is a design approach where a large application is divided into smaller, independent services. Each microservice handles a specific business function and communicates with others via APIs. This approach helps improve scalability, maintainability, and development speed. In this tutorial, we will focus on building a simple microservices setup using Node.js and Express.

Why use Node.js and Express? Node.js is lightweight and event-driven, making it ideal for building efficient network applications. Express is a minimal and flexible web framework that works well to create APIs quickly. Together, they allow you to build scalable microservices with ease.

### Step 1: Setup two simple microservices

Let's create two microservices. One will be a User Service to manage user data, and the other will be an Order Service. Each service runs independently.

First, create a folder for each service: `user-service` and `order-service`. Inside each, run `npm init -y` to initialize, then install Express with `npm install express`.

javascript
const express = require('express');
const app = express();
app.use(express.json());

const users = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' }
];

app.get('/users', (req, res) => {
  res.json(users);
});

app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (user) res.json(user);
  else res.status(404).send('User not found');
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`User Service running on port ${PORT}`);
});

This is the User Service code in `user-service/index.js`. It exposes simple endpoints to get all users or a specific user by ID.

javascript
const express = require('express');
const app = express();
app.use(express.json());

const orders = [
  { id: 1, userId: 1, product: 'Book' },
  { id: 2, userId: 2, product: 'Pen' }
];

app.get('/orders', (req, res) => {
  res.json(orders);
});

app.get('/orders/:id', (req, res) => {
  const order = orders.find(o => o.id === parseInt(req.params.id));
  if (order) res.json(order);
  else res.status(404).send('Order not found');
});

const PORT = 3001;
app.listen(PORT, () => {
  console.log(`Order Service running on port ${PORT}`);
});

Now, the Order Service code is placed in `order-service/index.js`. It manages orders and their association to users.

### Step 2: Running Services

Open two terminals to run the services simultaneously. In the `user-service` folder, run `node index.js`, and in the `order-service` folder, run `node index.js`. You now have two independent services running on different ports.

### Step 3: Communicating between Microservices

Microservices often need to communicate. You can use HTTP requests between services. For example, the Order Service might call the User Service to enrich order data with user details.

Here's how you can fetch user data from the Order Service using the `node-fetch` package.

javascript
const fetch = require('node-fetch');

app.get('/orders-with-user/:id', async (req, res) => {
  const order = orders.find(o => o.id === parseInt(req.params.id));
  if (!order) return res.status(404).send('Order not found');

  try {
    const userResponse = await fetch(`http://localhost:3000/users/${order.userId}`);
    if (!userResponse.ok) return res.status(502).send('Failed to fetch user');
    const user = await userResponse.json();

    res.json({ ...order, user });
  } catch (error) {
    res.status(500).send('Internal Server Error');
  }
});

In this example, we add a new endpoint `/orders-with-user/:id` in the Order Service that fetches order information and enriches it with user details by calling the User Service.

### Step 4: Scaling Microservices

Your microservices are easily scalable. Since they run independently, you can deploy each on separate servers or containers and scale them based on load. Using tools like Docker and Kubernetes can make this process seamless.

### Final Thoughts

This tutorial showed a very basic microservices setup with Node.js and Express. Real-world projects require attention to service discovery, load balancing, authentication, and asynchronous communication methods like message queues. Nonetheless, this foundation helps you start building scalable applications using microservices architecture.