DEV Community

Cover image for Creating Typescript app with decorator-based dependency injection 💉
Oleksandr Demian
Oleksandr Demian

Posted on • Edited on

Creating Typescript app with decorator-based dependency injection 💉

As a huge fan of Node.js and TypeScript, I love how these technologies offer a fast and flexible approach to building applications. However, this flexibility can be a double-edged sword. Code can quickly become messy, which leads to a decline in maintainability and readability over time.

Having worked extensively with Spring (Java) and NestJS (TypeScript), I’ve come to realize that Dependency Injection (DI) is a powerful pattern for maintaining code quality in the long term. With this in mind, I set out to explore how I could create a TypeScript library that would serve as a foundation for Node.js projects. My goal was to create a library that enforces a component-based development approach while remaining flexible and easily extensible for various use cases. This is how I came up with 🍋 Lemon DI.

How it Works

The core idea behind Lemon DI is quite similar to NestJS (though with different naming conventions). All classes decorated with @Component automatically become injectable components, while non-class entities (e.g., types or interfaces) can be instantiated using factories (@Factory) and injected using unique tokens.

Let’s walk through an example where we integrate with an SQLite database using TypeORM.

Setting Up

Start by installing the required dependencies:

npm install @lemondi/core reflect-metadata sqlite3 tsc typeorm typescript class-transformer 
Enter fullscreen mode Exit fullscreen mode

Since TypeORM is an external library, we’ll create the data source using a factory:

// factory/datasource.ts import {Factory, FilesLoader, Instantiate} from "@lemondi/core"; import {DataSource} from "typeorm"; // @Factory decorator marks this class as a provider of components through functions @Factory() export class DataSourceFactory { // @Instantiate decorator marks this function as a provider of a component @Instantiate({ qualifiers: [DataSource] // This tells DI that this function creates a DataSource component }) // This is an async function, which means the DI system will wait for it to resolve before using the component async createDatasource() { // create DataSource instance const ds = new DataSource({ type: "sqlite", // use sqlite for simplicity, but this works perfectly with any other DB database: ":memory:", synchronize: true, // Automatically create tables on startup // load all models entities: [FilesLoader.buildPath(__dirname, "..", "models", "*.entity.{js,ts}")], }); await ds.initialize(); // Initialize the DataSource before using it return ds; } } 
Enter fullscreen mode Exit fullscreen mode

Now that we have our DataSource component, let’s define a model and a service to interact with it:

// models/user.entity.ts import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"; import {plainToClass} from "class-transformer"; // This is a standard TypeORM entity declaration @Entity({ name: "users" }) export class User { @PrimaryGeneratedColumn("uuid") id?: string; @Column() firstName: string; @Column() lastName: string; static fromJson (json: User) { return plainToClass(User, json); } } 
Enter fullscreen mode Exit fullscreen mode
// services/UsersService.ts import {Component} from "@lemondi/core"; import {DataSource, Repository} from "typeorm"; import {User} from "../models/user.entity"; // This class is marked as component, it will automatically map itself during the dependency injection step @Component() export class UsersService { private repository: Repository<User>; // The component constructor is where the dependency injection happens // For each argument, the DI system will look for a component and provide it (the components are instantiated automatically when needed) constructor( // Here we tell DI system that we need DataSource instance (which is exported from our factory) // It is completely transparent for us that the DataSource component is async dataSource: DataSource, ) { this.repository = dataSource.getRepository(User); } save(user: User) { return this.repository.save(user); } find() { return this.repository.find(); } } 
Enter fullscreen mode Exit fullscreen mode

Now that we have our DB and Users service in place we can start our app:

import "reflect-metadata"; // this is required to emit classes metadata import {Component, FilesLoader, OnInit, start} from "@lemondi/core"; import {UsersService} from "./services/users"; import {User} from "./models/user.entity"; @Component() class App { constructor( private usersService: UsersService, ) { } // @OnInit decorator only works for components directly imported in `start` // @OnInit decorator tells the system to execute this method after the component is instantiated @OnInit() async onStart() { // create a new entry const user = User.fromJson({ lastName: "Last", firstName: "First", }); // save user in DB await this.usersService.save(user); // fetch user from DB const users = await this.usersService.find(); console.log(users); // will print data fetched from DB } } // start method is required to start the app start({ importFiles: [ // since there is no need to reference factories in the code, we need to tell our DI system to import those files to make sure they are accessible FilesLoader.buildPath(__dirname, "factories", "**", "*.js"), ], modules: [App], // The entry point; classes listed here will be instantiated automatically }); 
Enter fullscreen mode Exit fullscreen mode

TypeScript Configuration

To enable decorators and ensure everything works as expected, add the following to your tsconfig.json:

{ "compilerOptions": { "lib": ["es5", "es6", "dom"], "target": "es5", "module": "commonjs", "moduleResolution": "node", "outDir": "./dist", "experimentalDecorators": true, "emitDecoratorMetadata": true } } 
Enter fullscreen mode Exit fullscreen mode

Finally, run the following command to compile and execute the app:

tsc && node ./dist/app.js 
Enter fullscreen mode Exit fullscreen mode

Final thoughts

⚠️ Important: Please note that this library is still in its early stages and should not be used in production applications yet. It’s a prototype I created for fun and exploration of decorators in TypeScript. You can find full example code here.

Top comments (0)