Search code examples
typescriptexpressdependency-injectionarchitecturecompositionroot

Implementation of Composition Root in typescript express


I am trying to understand how should I implement a composition root in a project.

From what I have red, if use the composition root the wrong way (for example, by referencing it in lots of place in your application code), you will end up with the service locator.

Let me show you an example of a projcet with out a compositon root.

I have the following project structure:

  • server.ts
  • domain.ts
  • application.ts
  • api.ts
  • sql-repository

server.ts:

This file imports the API and initializes the server.

import express from 'express';
import API from './api'

const app = express();
const port = 3000;

app.use(express.json());

app.use(API);

// Start server
app.listen(port, () => {
    console.log('listening on port: ' + port);
});

domain.ts:

This file holds the core logic of the domain.

export type Entity = {
    param1: string,
    param2: string,
};

export type IRepository = {
    GetMultipleEntities(filterParam: string): Entity[] | undefined
    GetEntity(filterParam: string): Entity | undefined
    CreateEntity(entity: Entity): void
    UpdateEntity(entity: Entity): void
}

application.ts:

This file holds the use cases of the application.

import {IRepository} from './domain';

export const CheckIfEntityExists = (filterParam: string, entityRepository: IRepository): boolean => {
    let entity = entityRepository.GetEntity(filterParam);
    return typeof entity != "undefined";
};

sql-repository.ts:

This file holds the concrete implementation of the IRepository interface

import {Entity, IRepository} from './domain';

export class SqlRepository implements IRepository {
    GetEntity(filterParam: string): Entity {
        //
        // some sort of logic to get entity from an sql database
        //
        return {
            param1: '',
            param2: ''
        };
    }
    GetMultipleEntities(filterParam: string): Entity[] {
        //
        // some sort of logic to get multiple entity from an sql database
        //
        return [
            {
                param1: '',
                param2: ''
            },
            {
                param1: '',
                param2: ''
            }
        ];
    }
    CreateEntity(entity: Entity): void {
        // some logic to enter new data to the sql database that represents an entity
    }
    UpdateEntity(entity: Entity): void {
        // some logic to update the entity
    }
}

api.ts:

This file holds the api that uses the use cases in the application.ts file

import {Router} from 'express'
import {CheckIfEntityExists} from './application';
import {SqlRepository} from './sql-repository';

const router = Router();

router.get("/exists/:filterParam", async (req, res) => {
    CheckIfEntityExists(req.params.filterParam, new SqlRepository);
    res.end()
});

export default router

Ofc this is just an example, but you get the point of how the project looks like.

From what you can see, its all good until we see the api.ts file. It imports the concrete implementation and injects it into the use case. What if there were much more dependencies to import and use, I do not want the api.ts to be responsible to decide which implementations go to which place, its not its responsibility.

But on the other hand, how should I implement a composition root then? I have no idea how should I construct the full object graph and then pass it to the server object so that the right implementation will go to the right objects.

Thanks in advance!


Solution

  • Definitions

    To give some scope and definitions of the term Composition Root, here are good quotes by Mark Seemann in two related articles:

    Where should we compose object graphs?

    As close as possible to the application's entry point.

    What is a Composition Root?

    A Composition Root is a (preferably) unique location in an application where modules are composed together.

    The Composition Root is an application infrastructure component.

    A Composition Root is application-specific; it's what defines a single application. After having written nice, decoupled code throughout your code base, the Composition Root is where you finally couple everything, from data access to (user) interfaces.

    Implications

    In other words, your api.ts could be seen as the entry point of your server application, so it is perfectly fine to compose your object graph in it. You could also

    1. choose server.ts or
    2. define a separate DI module like composition-root.ts which does all the composing and is imported by server.ts or api.ts (even more cohesive).

    More important here is, that you have a unique location near/in your application entry point of your project which is responsible for creating/composing the dependencies.

    Example

    Let's take your concrete example and presume we want to do all composing stuff in composition-root.ts imported by api.ts. Your dependency graph looks like this (--> means an import here):

    server.ts --> api.ts --> application.ts --> domain.ts 
                                            --> sql-repository.ts
    

    Everything except composition-root.ts is decoupled from its dependencies. Constructor injection could be used like in the article's example, or any another injection method, depending on the language/framework/coding style. Your sample already looks quite fine, let's add some DB abstraction layer for the repository and abstract the composing away from api.ts.

    sql-repository.ts:

    export class SqlRepository implements IRepository {
      constructor(private db: DB) {}
      ...
    }
    

    api.ts:

    import {CheckIfEntityExists} from "./composition-root"
    ...
    
    router.get("/exists/:filterParam", async (req, res) => {
        CheckIfEntityExists(req.params.filterParam);
        res.end()
    });
    

    composition-root.ts:

    import {CheckIfEntityExists} from './application';
    import {SqlRepository} from './sql-repository';
    
    const db = new DB();
    const sqlRepository = new SqlRepository(db);
    // We do partial application here to free api.ts from 
    // knowing the concrete repository.
    const _CheckIfEntityExists = (filterParam: string) =>
      CheckIfEntityExists(filterParam, sqlRepository);
    
    export { _CheckIfEntityExists as CheckIfEntityExists };
    

    All in all, you have encapsulated your dependencies nicely in one place composition-root.ts, and other code in the more inner layers of your application architecture does not know anything about their construction.

    Hope, that helps.