October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk5 min

I Missed `@Service` in Node.js, So I Built a Service Layer for Express

A practical TypeScript pattern for registering services and injecting them into Express routes, with explicit wiring, lifecycle guidance, and a test fake.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Express does not include an @Service decorator or a dependency-injection container. It provides routes and middleware; service registration, construction, dependency resolution, and lifecycle are application-level choices. A small, explicit registry can reproduce the useful part of the convention without pretending the decorator does the work by itself.

This example targets Express 5 and TypeScript. Express 5’s application API currently lists Node.js >=20.19.3 <21 || >=22.2.0 as its requirement; check the versioned Express 5 application documentation against your project before adopting that runtime range.

As an Amazon Associate I earn from qualifying purchases.

What “@Service” needs to do

Express describes itself as a routing and middleware web framework with minimal functionality of its own: an app is essentially middleware functions executed during the request-response cycle. Its documented building blocks are route handlers and routers, not a service container. See Express’s middleware guide and routing guide.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A service convention has several distinct jobs:

  • Marking: identify a class as a service.
  • Registration: make it available under a runtime token.
  • Construction: decide when and how to create it.
  • Resolution: supply dependencies to consumers.
  • Lifecycle: decide whether an instance is shared or created per request.

A decorator can help with marking or registration, but it does not automatically solve the other jobs. The implementation below keeps construction and dependency wiring explicit, then uses a decorator only as a registry marker.

A small TypeScript implementation

Use class constructors as tokens. This keeps the example type-aware and avoids relying on TypeScript interfaces at runtime: interfaces are erased when TypeScript compiles, so they cannot identify a dependency in a JavaScript container.

1. Create a registry and decorator

type Constructor<T = object> = new (...args: any[]) => T;

const serviceTypes = new Set<Constructor>();

function Service<T extends Constructor>(target: T): T {
  serviceTypes.add(target);
  return target;
}

class Container {
  private readonly instances = new Map<Constructor, unknown>();

  register<T>(token: Constructor<T>, instance: T): void {
    this.instances.set(token, instance);
  }

  resolve<T>(token: Constructor<T>): T {
    if (!this.instances.has(token)) {
      throw new Error(`Service not registered: ${token.name}`);
    }

    return this.instances.get(token) as T;
  }
}

@Service adds a class to a set; it does not instantiate the class. The container stores instances by constructor and fails clearly when a dependency has not been registered.

2. Define a service and its dependency

class UserRepository {
  findName(id: string): string | undefined {
    return id === "42" ? "Ada" : undefined;
  }
}

@Service
class UserService {
  constructor(private readonly users: UserRepository) {}

  getName(id: string): string | undefined {
    return this.users.findName(id);
  }
}

The decorator registry can be used to check which classes were marked, but it does not infer the constructor parameter or decide how to create it. Wiring stays in one composition function:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function buildContainer(): Container {
  const container = new Container();
  const users = new UserRepository();
  const userService = new UserService(users);

  container.register(UserRepository, users);
  container.register(UserService, userService);

  return container;
}

Explicit construction is intentionally visible: the repository is created first, passed to the service, and both are registered. For a larger application, this wiring can be moved into modules or replaced with a full container, but the lifecycle decisions remain important.

Connect the service to an Express route

Express route handlers can be functions, arrays of functions, or combinations, and routers provide modular, mountable routing. A handler factory is a direct integration point: resolve what the route needs once and close over it.

import express from "express";

function createUserRouter(container: Container) {
  const router = express.Router();
  const users = container.resolve(UserService);

  router.get("/:id", (req, res) => {
    const name = users.getName(req.params.id);

    if (!name) {
      res.sendStatus(404);
      return;
    }

    res.json({ id: req.params.id, name });
  });

  return router;
}

const app = express();
const container = buildContainer();

app.use("/users", createUserRouter(container));
app.listen(3000);

The route is mounted at /users, so a request to /users/42 reaches the router’s /:id handler. This handler uses a shared service instance because buildContainer() creates it once and the router closes over it. That is appropriate for stateless services or shared infrastructure clients; it is not appropriate for storing one user’s request data on the service instance.

Choose a lifecycle deliberately

Singleton and request-scoped are different behaviors, not interchangeable ways to write the same registration. A singleton is one instance reused across requests. A request-scoped object is created for, or associated with, an individual request. Do not put request-specific mutable state in a singleton.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Use shared instances for stateless domain services and intentionally shared clients whose own lifecycle is managed appropriately.
  • Pass request data as method arguments when a service needs the current user, locale, or transaction context but does not need a request-specific instance.
  • Introduce request scope only when needed. It requires per-request creation and a clear way to resolve that request’s dependencies; it should not be simulated by mutating a global singleton.

For an example of a different Express integration pattern, the Awilix Express package listing surfaces container registration, per-request scope, and controller/route decorators. That demonstrates an existing approach, not a claim about the current release’s recency or a requirement to use it.

Replace a dependency in a test

Because construction is explicit, a test can provide a fake repository without changing global state or patching modules:

class FakeUserRepository extends UserRepository {
  findName(id: string): string | undefined {
    return id === "test-user" ? "Test Person" : undefined;
  }
}

const container = new Container();
const repository = new FakeUserRepository();
const service = new UserService(repository);

container.register(UserRepository, repository);
container.register(UserService, service);

if (container.resolve(UserService).getName("test-user") !== "Test Person") {
  throw new Error("Unexpected service result");
}

The example checks service wiring directly. An HTTP-level test can additionally mount createUserRouter(container) in an Express app and assert the response, using the project’s chosen test runner and HTTP test client.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When a decorator or container is the wrong tool

For a small Express app, a function that constructs and passes dependencies may be clearer than a registry. A custom decorator becomes useful when it removes repetitive registration without hiding important construction rules. If the application needs nested scopes, automatic construction, or extensive binding conventions, an established container may be preferable to extending this minimal example.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frameworks document different parts of the distinction. LoopBack’s service decorator documentation describes injection of an instance matching a service binding; it also calls for a string or symbol token when an interface is not a class, and requires the service to be bound in the context. Ts.ED’s providers documentation distinguishes AutoInjectable behavior when a class is instantiated with new from provider registration that makes a class injectable into other classes. In both cases, a decorator should not be confused with registration, binding, or lifecycle policy.

JavaScript and TypeScript caveats

This example uses TypeScript class decorators and constructor types, but does not use reflection metadata or infer constructor arguments. That is why manual construction is explicit and why the example does not need to treat an interface as a runtime token. In plain JavaScript, the same registry and container can be written with constructor functions or classes, but TypeScript’s compile-time types will not be available.

If you adopt a decorator implementation that depends on emitted metadata, check its requirements against the TypeScript compiler configuration, decorator version, and runtime. Those details are not established by Express itself. Keep the Express major version and Node.js runtime aligned with the project’s lockfile and deployment environment; the versioned Express 5 API page states the Node requirement noted above.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.