When building modern Node.js and Express applications, managing routing and dependency injection can become increasingly complex. Handling controllers, services, and middleware without clean separation often leads to code that is harder to maintain. However, using the right tools and design patterns, we can significantly simplify this process.
In this article, I’ll walk you through how to build a Node.js application with decorators-based routing and dependency injection using the @lemondi library.
Why Use Decorators and Dependency Injection?
Decorators are a powerful feature in TypeScript and JavaScript that allow you to add metadata to classes, methods, and properties. With decorators, we can annotate routing methods, define their HTTP methods, and handle dependency injection without writing complex boilerplate code.
Dependency injection (DI) helps manage how services are instantiated and injected into other components. It decouples the components from each other, making your application more modular and testable. In our case, we’ll use DI for services like database connections and routing.
The @lemondi library simplifies the process by automating DI, handling decorators, and reducing the need for boilerplate code. Let’s dive into how it works!
1. Project Setup
Before we start building, let’s ensure we have the required libraries installed:
npm init -y
npm install express reflect-metadata @lemondi/core @lemondi/scanner typeorm sqlite3 class-transformer
npm install --save-dev typescript @types/node @types/express
Next, let’s set up TypeScript by creating a tsconfig.json
file:
{
"compilerOptions": {
"lib": ["es5", "es6", "dom"],
"target": "es5",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
This configuration enables TypeScript's support for decorators and metadata reflection, which is required by the @lemondi
library.
2. Decorators for Routing and Injection
routing.ts
- Creating the Decorators
First, let’s create two decorators: one for classes (@Router
) to define a router, and another for methods (@Route
) to define HTTP routes.
// file: src/decorators/routing.ts
import { createClassDecorator, createMethodDecorator } from "@lemondi/scanner";
// Enum for HTTP methods
export enum HttpMethod {
GET = "get",
POST = "post",
PUT = "put",
DELETE = "delete",
PATCH = "patch",
OPTIONS = "options",
}
// @Router decorator for class routing
export const Router = createClassDecorator<{ path: string }>("Router");
// @Route decorator for method routing
export const Route = createMethodDecorator<{ path: string; method: HttpMethod }>("Route");
Here, we use @lemondi/scanner
's createClassDecorator
and createMethodDecorator
to simplify the creation of decorators for routing.
3. Defining the Data Source
datasource.ts
- The DataSource Factory
We’ll need a way to create and inject a DataSource
(e.g., for connecting to a database). This is where the @lemondi
library's @Factory
and @Instantiate
decorators come into play.
// file: src/factories/datasource.ts
import { Factory, FilesLoader, Instantiate } from "@lemondi/core";
import { DataSource } from "typeorm";
@Factory()
export class DataSourceFactory {
@Instantiate({ qualifiers: [DataSource] })
async createDatasource() {
const ds = new DataSource({
type: "sqlite",
database: ":memory:",
synchronize: true,
entities: [FilesLoader.buildPath(__dirname, "..", "models", "*.entity.{js,ts}")],
});
await ds.initialize();
return ds;
}
}
The @Factory
decorator marks DataSourceFactory
as a provider of components, while @Instantiate
marks the createDatasource
method as a provider for the DataSource
component. DI will automatically resolve and inject the required DataSource
.
4. Defining the Entity
user.entity.ts
- A TypeORM Entity
Here’s a simple TypeORM entity to define a User
model.
// file: src/models/user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { plainToClass } from "class-transformer";
@Entity({ name: "users" })
export class User {
@PrimaryGeneratedColumn("uuid")
id?: string;
@Column()
firstName: string;
@Column()
lastName: string;
static fromJson(json: User) {
return plainToClass(User, json);
}
}
This entity represents a User
in the database with fields firstName
and lastName
. We also provide a utility function fromJson
to easily convert JSON data to an instance of the User
class.
5. Creating the Router
UsersRouter.ts
- Defining Routes
With the decorators in place, we can now define our UsersRouter
class to handle user-related routes.
// file: src/routers/UsersRouter.ts
import { HttpMethod, Route, Router } from "../decorators/routing";
import { UsersService } from "../services/UsersService";
import { Request } from "express";
import { User } from "../models/user.entity";
@Router({ path: "/users" })
export class UsersRouter {
constructor(private readonly usersService: UsersService) {}
@Route({ path: "/", method: HttpMethod.GET })
getUsers() {
return this.usersService.find();
}
@Route({ path: "/", method: HttpMethod.POST })
createUser(req: Request) {
const data = User.fromJson(req.body);
return this.usersService.save(data);
}
}
Here, the @Router
decorator defines the base path /users
, and the @Route
decorators handle GET and POST methods for retrieving and creating users.
6. Service Layer
UsersService.ts
- Handling Business Logic
We define the service that interacts with the database.
// file: src/services/UsersService.ts
import { Component } from "@lemondi/core";
import { DataSource, Repository } from "typeorm";
import { User } from "../models/user.entity";
@Component()
export class UsersService {
private repository: Repository<User>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(User);
}
save(user: User) {
return this.repository.save(user);
}
find() {
return this.repository.find();
}
}
The UsersService
class is decorated with @Component()
, and its constructor automatically injects the DataSource
instance. This allows the service to perform database operations without any manual instantiation.
7. Bootstrapping the Application
app.ts
- Putting Everything Together
Finally, we initialize the application using the @lemondi
DI system and bind routes dynamically.
// file: src/app.ts
import "reflect-metadata";
import { Component, FilesLoader, instantiate, OnInit, start } from "@lemondi/core";
import * as express from "express";
import { findClassDecorators, findMethodDecorators, scan } from "@lemondi/scanner";
import { Route, Router } from "./decorators/routing";
@Component()
class App {
@OnInit()
async onStart() {
const server = express();
server.use(express.json());
const routers = scan(Router);
for (const router of routers) {
const routerInstance = await instantiate(router);
const [routerDecorator] = findClassDecorators(router, Router);
for (const prop of Reflect.ownKeys(router.prototype)) {
const [props] = findMethodDecorators(router, prop, Route);
if (props) {
const url = routerDecorator.decoratorProps.path + props.decoratorProps.path;
server[props.decoratorProps.method](url, async (...args) => {
const result = await Promise.resolve(routerInstance[prop].call(routerInstance, ...args));
args[1].json(result).end();
});
}
}
}
server.listen(3000);
}
}
start({
importFiles: [
FilesLoader.buildPath(__dirname, "factories", "**", "*.js"),
FilesLoader.buildPath(__dirname, "routers", "**", "*.js"),
],
modules: [App],
});
Here, we use the @OnInit
decorator to initialize the Express server after the application is instantiated. We dynamically scan for @Router
and @Route
decorators and configure routes on the server.
You can now run the app using the following command:
tsc && node ./dist/app.js
Conclusion
Using decorators and the DI system provided by @lemondi
, we’ve simplified our Node.js and Express application. This approach abstracts away much of the boilerplate code typically required for routing and dependency management, leading to cleaner, more maintainable code.
If you’re tired of manually configuring routing and services, this pattern is definitely worth exploring. By using decorators, we can keep the code more declarative, readable, and modular.
Top comments (0)