Skip to content
Express.js ex microservices 4 min read

Microservices with Express

A microservice architecture splits an application into small, independently deployable services that each own one business capability and talk to each other over the network. It trades the simplicity of a single deployable for independent scaling, isolated failures, and team autonomy. Express’s minimal, unopinionated footprint makes it a natural fit for writing those individual services. This page defines microservices against the monolith, explains when (and when not) to split, and shows where Express slots in.

Monolith vs microservices

A monolith packages every feature — auth, billing, catalog, notifications — into one codebase and one deployable process. Everything shares a database and is released together. A microservice system breaks those features into separate services, each with its own codebase, deployment pipeline, and usually its own datastore, communicating over HTTP, gRPC, or a message broker.

ConcernMonolithMicroservices
DeploymentOne artifact, all-or-nothingPer-service, independent
ScalingWhole app scales togetherScale hot services only
DataShared databaseDatabase per service
Failure blast radiusProcess-wideIsolated to a service
Team workflowCoordinated releasesAutonomous teams
Operational costLowHigh (networking, observability)

Microservices solve an organizational and scaling problem, not a coding-style problem. If a single team ships a small app, a well-structured monolith is almost always the right call. Distribution adds latency, partial failures, and deployment complexity you don’t want until you need it.

When to split

Splitting is justified when parts of the system have genuinely different requirements. Reach for separate services when:

  • A component must scale independently — e.g. an image-processing worker that needs CPU while the API stays I/O-bound.
  • Teams keep blocking each other on a shared deploy pipeline.
  • A subsystem has a different reliability or compliance profile (payments, PII) that you want isolated.
  • You need independent release cadence — ship the recommendations engine ten times a day without redeploying checkout.

Avoid premature splitting. A distributed monolith — many services that must all be deployed together and share a database — gives you every cost of microservices with none of the benefits.

Bounded contexts

The hardest part is where to draw the lines. Domain-Driven Design’s concept of a bounded context is the standard answer: a service should own one cohesive slice of the domain, including its data and the language used to describe it. An Order in the Ordering context and a Customer in the Billing context are different models with different rules, even if they reference the same person.

A clean boundary means a service can be understood, deployed, and changed without reaching into another service’s database. The litmus test: if two “services” can’t be deployed independently because they share tables, they belong in the same context.

Why Express for a service

Express is a thin layer over Node’s HTTP server, which is exactly what you want for a single-purpose service: a small dependency surface, fast startup, and full control over routing and middleware. Each service is its own Express app with its own port, routes, and Router modules.

// order-service/app.js
import express from "express";
import { Router } from "express";

const app = express();
app.use(express.json());

const orders = Router();

orders.post("/", async (req, res, next) => {
  try {
    const { items, customerId } = req.body;
    const order = await createOrder(customerId, items);
    res.status(201).json(order);
  } catch (err) {
    next(err); // Express 4.x: forward async errors manually; 5.x does this for you
  }
});

orders.get("/:id", async (req, res, next) => {
  try {
    const order = await findOrder(req.params.id);
    if (!order) return res.status(404).json({ error: "Order not found" });
    res.json(order);
  } catch (err) {
    next(err);
  }
});

app.use("/orders", orders);

// service-local error handler
app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Internal Server Error" });
});

app.listen(process.env.PORT ?? 4001, () => {
  console.log(`order-service on :${process.env.PORT ?? 4001}`);
});

On Express 5.x, async handlers that reject are forwarded to the error middleware automatically, so the try/catch + next(err) boilerplate becomes optional. On 4.x you must still catch and forward yourself.

A POST /orders with a JSON body returns the created resource:

Output:

HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "ord_8a1f",
  "customerId": "cus_204",
  "items": [{ "sku": "BK-12", "qty": 2 }],
  "status": "pending"
}

Every service follows this same shape, so the pattern is uniform across the system. A health-check endpoint is essential for orchestrators like Kubernetes to know the service is alive:

app.get("/healthz", (req, res) => res.json({ status: "ok" }));

Communication styles

Services rarely live alone — they call each other. There are two broad styles, and most systems use both. Synchronous request/response (HTTP or gRPC) is simple and immediate but couples caller availability to callee availability. Asynchronous messaging through a broker (Kafka, RabbitMQ) decouples services in time and absorbs load spikes, at the cost of eventual consistency. An API gateway typically fronts the whole system, giving clients one entry point while routing to the right service behind it.

Best Practices

  • Start with a modular monolith; extract a service only when scaling, ownership, or reliability demands it.
  • Give each service its own database — shared tables are the hallmark of a distributed monolith.
  • Keep each Express service small and single-purpose, with its own port, repo, and pipeline.
  • Expose a /healthz (and optionally /readyz) endpoint for orchestration and load balancers.
  • Make remote calls resilient: add timeouts, retries with backoff, and circuit breakers — the network will fail.
  • Prefer asynchronous messaging for workflows that can tolerate eventual consistency to reduce coupling.
  • Invest early in observability (structured logs, request IDs, distributed tracing) — debugging is harder once calls cross the wire.
Last updated June 14, 2026
Was this helpful?