The Adaptable Backend: Node.js REST API with Dependency Injection

By Hendrix Roa
July 18, 2025
5 min read
Posted in The Adaptable Backend: Node.js
The Adaptable Backend: Node.js REST API with Dependency Injection

Dependency Injection (DI) can be a controversial topic. It’s interpreted differently across stacks, sparks lengthy forum debates, and fuels “best practice” posts from LinkedIn gurus — often with no working code to back them up. With so many libraries and framework-specific implementations, confusion is common.

Common Problems in DI

  • Circular dependencies.
  • Multiple DI libraries used within the same project.
  • Unmaintainable code due to excessive dependencies.
  • Memory leaks caused by poor object lifecycle management.

Dependency injection issues collage

Available Options

In the TypeScript/Node.js ecosystem, several DI libraries are available:

I chose Awilix because it aligns most closely with the principles Martin Fowler outlines in his article on Dependency Injection. Another reason I favor Awilix is its strong support for TypeScript and Node.js as first-class citizens — a philosophy that aligns with the goals of this series.

Preparing the Example for DI

In the previous article, we created a RestController capable of handling a basic method. We also integrated a dependency-free library to manage environment variables in a resilient way. We organized the project into two main folders: apps, which holds technology-specific code like REST or GraphQL APIs, and core, which contains the business logic.

Now, let’s create a NoteController responsible for managing the logic related to Notes. Inside src/core, create a features folder, and within it, a notes directory. We’re intentionally avoiding a flat structure where all controllers, services, and models live in the same place. This allows each feature to grow independently and integrate with external services at its own pace.

The note.dto.ts file has been moved from the RestControllers folder to features/notes, since DTOs belong in the core layer where the business logic resides.

Using Aliases with tsconfig-paths

We installed the tsconfig-paths library to simplify import statements and improve readability. Update tsconfig.json with these aliases:

tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist",
...
"paths": {
"src/*": ["src/*"],
"@core/*": ["src/core/*"],
"@restAPI/*": ["src/apps/restAPI/*"]
}
},
...
}

Now, create NoteController, which encapsulates the business logic.

src/core/features/notes/noteController.ts
import { NoteDto } from "@core/features/note/note.dto";
export class NoteController {
public getNotes(): NoteDto[] {
return [
{
id: 1,
content: "Hello 1 - from controller",
},
{
id: 2,
content: "Hello 2 - from controller",
},
];
}
}

And update NoteRestController as follows:

src/apps/restAPI/frameworks/nestjs/restControllers/noteRestController.ts
import { Controller, Get } from "@nestjs/common";
import { ApiOkResponse } from "@nestjs/swagger";
import { NoteDto } from "./note.dto";
import { NoteDto } from "@core/features/note/note.dto";
import { NoteController } from "@core/features/note/noteController";
@Controller("notes")
export class NoteRestController {
private noteController: NoteController;
constructor() {
this.noteController = new NoteController();
}
@Get()
@ApiOkResponse({
type: [NoteDto],
description: "Get all notes",
})
public getNotes(): NoteDto[] {
return [
{ id: 1, content: "Hello 1" },
{ id: 2, content: "Hello 2" },
];
return this.noteController.getNotes();
}
}

This decouples framework-specific logic (NestJS) from the business logic (pure TypeScript).

Run the REST API again:

Running restAPI
npm run start:dev:restAPI

Visiting the Swagger docs and hitting the GET /notes endpoint, you’ll see the updated response.

The Problem DI Solves

In this setup, we’re manually instantiating NoteController. But what happens as dependencies grow — both in the controller and the API layer? You’ll end up with a tangled web of constructor calls, hard-to-trace bugs, and code that’s difficult to test or scale.

Dependency injection issues solves

Installing & Configuring Awilix

Install the library:

Installing awilix
npm i awilix

The core concept of Awilix is a container: a centralized object that manages your dependencies. This makes your code predictable, testable, and less prone to hidden errors like circular dependencies or undefined injections.

Creating the Container

By convention, define the container in a dedicated file. Here’s a minimal example with strict: true, which helps catch issues during transpilation.

src/core/container/container.ts
import { NoteController } from "@core/features/note/noteController";
import { createContainer, asClass, InjectionMode } from "awilix";
export const container = createContainer({
injectionMode: InjectionMode.CLASSIC,
strict: true,
});
container.register({
noteController: asClass(NoteController).singleton(),
});

Updating the NoteRestController

Now, inject the NoteController using Awilix:

src/apps/restAPI/frameworks/nestjs/restControllers/noteRestController.ts
import { Controller, Get } from "@nestjs/common";
import { ApiOkResponse } from "@nestjs/swagger";
import { NoteDto } from "@core/features/note/note.dto";
import { NoteController } from "@core/features/note/noteController";
import { container } from "@core/container/container";
@Controller("notes")
export class NoteRestController {
private noteController: NoteController;
constructor() {
this.noteController = new NoteController();
this.noteController = container.resolve("noteController");
}
@Get()
@ApiOkResponse({
type: [NoteDto],
description: "Get all notes",
})
public getNotes(): NoteDto[] {
return this.noteController.getNotes();
}
}

When you restart the project, you won’t notice any visual changes — but under the hood, you’ve eliminated future headaches by standardizing how dependencies are handled.

Conclusion

With minimal changes, we’ve separated business logic from framework-specific concerns and prepared our app to grow cleanly. This structure allows us to scale confidently by adding services, persistence layers, and more — without coupling logic to implementation details.

In the next article, we’ll implement full CRUD (Create, Read, Update, Delete) operations for Notes with database integration.

Have you dealt with tangled dependencies or scaling challenges in your backend projects?
Share your experience, ask questions, or explore the GitHub repo and try the setup yourself.

Comments

Loading comments...

You Might Also Like