decorator-koa-router

Package to declare koa controllers via decorators

Usage no npm install needed!

<script type="module">
  import decoratorKoaRouter from 'https://cdn.skypack.dev/decorator-koa-router';
</script>

README

Decorator koa router

Usage

This library streamlines the declaration of routes, allowing an agile development of the APIs.

The integration has been done under the official package of the Koa team: https://www.npmjs.com/package/@koa/router

To use this library it is necessary to work with a dependency container.

Examples have been made with the InversifyJS and node-dependency-injection packages.

The examples are in this repository: test-koa-router

You can integrate any other dependency injector as long as it complies with the "IocAdapter" interface

Example with InversifyJS

Server.ts

import 'reflect-metadata';
import Koa from 'koa';
import { join } from 'path';
import { InversifyIocAdapter } from './InversifyIocAdapter';
import { RouterBuilder } from 'decorator-koa-router';

export class Server {
  public static async load(): Promise<void> {
    const port = 3000;
    const koaServer = new Koa();
    
    const controllersPath = join(__dirname, './Controllers/*.ts');// The path where controllers are
    const iocAdapter = new InversifyIocAdapter();
    const router = await RouterBuilder('/api', iocAdapter, controllersPath);
    
    koaServer.use(router);
    koaServer.listen(port);
    console.info(`Server running at port ${port}`);
  }
}
Server.load();

InversifyIocAdapter

import { IocAdapter } from '../../IocAdapter';
import { Container } from 'inversify';
import { AdapterTypes } from './AdapterTypes';
import { ProductContainer } from './Containers/ProductContainer';
import { UserContainer } from './Containers/UserContainer';

export class InversifyIocAdapter implements IocAdapter {
  private _container: Container = new Container();

  constructor() {
    this.inject(ProductContainer.container());
    this.inject(UserContainer.container());
  }

  public get<T>(className: string): T {
    return this.container().get<T>(className);
  }

  private container(): Container {
    return this._container;
  }

  private inject(dependencies: AdapterTypes) {
    const { controllers, services } = dependencies;
    if (controllers) this.bindControllers(controllers);
    if (services) this.bindServices(services);
  }

  private bindControllers(controllers: AdapterTypes['controllers']) {
    if (controllers) {
      for (const controller of controllers) {
        this.container().bind(controller.name).to(controller).inRequestScope();
      }
    }
  }

  private bindServices(services: AdapterTypes['services']) {
    if (services) {
      for (const service of services) {
        this.container().bind(service.name).to(service).inRequestScope();
      }
    }
  }
}

ProductContainer

import { AdapterTypes } from '../AdapterTypes';
import { ProductController } from '../Controllers/ProductController';
import { ProductFinder } from '../Services/Product/ProductFinder';
import { ProductCreator } from '../Services/Product/ProductCreator';
import { ProductModifier } from '../Services/Product/ProductModifier';
import { ProductRemover } from '../Services/Product/ProductRemover';

export class ProductContainer {
  public static container(): AdapterTypes {
    return {
      controllers: [ProductController],
      services: [ProductFinder, ProductCreator, ProductModifier, ProductRemover],
    };
  }
}

ProductController

import { Context, Response } from 'koa';
import { ProductFinder } from '../Services/Product/ProductFinder';
import { controller, httpDelete, httpGet, httpPost, httpPut } from 'decorator-koa-router';
import { inject, injectable } from 'inversify';
import { ProductCreator } from '../Services/Product/ProductCreator';
import { ProductModifier } from '../Services/Product/ProductModifier';
import { ProductRemover } from '../Services/Product/ProductRemover';

@controller('/product')
@injectable()
export class ProductController {
  constructor(
    @inject('ProductFinder') private readonly productFinder: ProductFinder,
    @inject('ProductCreator') private readonly productCreator: ProductCreator,
    @inject('ProductModifier') private readonly productModifier: ProductModifier,
    @inject('ProductRemover') private readonly productRemover: ProductRemover
  ) {}

  @httpGet('/')
  public async listAll({ response }: Context): Promise<Response> {
    const product = await this.productFinder.run();
    response.status = 200;
    response.body = product;
    return response;
  }

  @httpPost('/')
  public async create({ response }: Context): Promise<Response> {
    await this.productCreator.run();
    response.status = 201;
    return response;
  }

  @httpPut('/:productId')
  public async modify({ response }: Context): Promise<Response> {
    await this.productModifier.run();
    response.status = 204;
    return response;
  }

  @httpDelete('/:productId')
  public async remove({ response }: Context): Promise<Response> {
    await this.productRemover.run();
    response.status = 200;
    return response;
  }
}

ProductFinder.ts (service)

import { injectable } from 'inversify';

@injectable()
export class ProductFinder {
  public async run(): Promise<Record<string, string | number>> {
    return {
      id: 1,
      name: 'test',
    };
  }
}

Example with node-dependency-injection

server.ts

import 'reflect-metadata';
import Koa from 'koa';
import { join } from 'path';
import { RouterBuilder } from '../../RouterBuilder';
import { RouterBuilder } from 'decorator-koa-router';

export class Server {
  public static async load(): Promise<void> {
    const port = 3000;
    const koaServer = new Koa();
    const controllersPath = join(__dirname, './Controllers/*.ts');
    const iocAdapter = new NodeDependencyInjectionIocAdapter();
    const router = await RouterBuilder('/api', iocAdapter, controllersPath);
    koaServer.use(router);
    koaServer.listen(port);
    console.info(`Server running at port ${port}`);
  }
}
Server.load();

NodeDependencyInjectionIocAdapter.ts

import { IocAdapter } from '../../IocAdapter';
import { join } from 'path';
import { ContainerBuilder, YamlFileLoader } from 'node-dependency-injection';

export class NodeDependencyInjectionIocAdapter implements IocAdapter {
  private readonly container: ContainerBuilder;

  constructor() {
    this.container = new ContainerBuilder();
    const loader = new YamlFileLoader(this.container);
    loader.load(join(__dirname, './Containers/Container.yaml'));
  }

  get<T>(className: string): T {
    return this.container.get<T>(className);
  }
}

ProductController.ts

import { Context, Next, Response } from 'koa';
import { ProductFinder } from '../Services/Product/ProductFinder';
import { controller, httpDelete, httpGet, httpPost, httpPut } from 'decorator-koa-router';
import { ProductCreator } from '../Services/Product/ProductCreator';
import { ProductModifier } from '../Services/Product/ProductModifier';
import { ProductRemover } from '../Services/Product/ProductRemover';

@controller('/product', (ctx: Context, next: Next) => {
  console.info('Middleware 1');
  next();
}, (ctx: Context, next: Next) => {
  console.info('Middleware 2...');
  next();
})
export class ProductController {
  constructor(
    private readonly productFinder: ProductFinder,
    private readonly productCreator: ProductCreator,
    private readonly productModifier: ProductModifier,
    private readonly productRemover: ProductRemover
  ) {
  }

  @httpGet('/', (ctx: Context, next: Next) => {
    console.info('Middleware 3');
    next();
  }, (ctx: Context, next: Next) => {
    console.info('Middleware 4...');
    next();
  })
  public async listAll({response}: Context): Promise<Response> {
    const product = await this.productFinder.run();
    response.status = 200;
    response.body = product;
    return response;
  }

  @httpPost('/')
  public async create({response}: Context): Promise<Response> {
    await this.productCreator.run();
    response.status = 201;
    return response;
  }

  @httpPut('/:productId')
  public async modify({response}: Context): Promise<Response> {
    await this.productModifier.run();
    response.status = 204;
    return response;
  }

  @httpDelete('/:productId')
  public async remove({response}: Context): Promise<Response> {
    await this.productRemover.run();
    response.status = 200;
    return response;
  }
}

ProductFinder.ts (service)

export class ProductFinder {
  public async run(): Promise<Record<string, string | number>> {
    return {
      id: 1,
      name: 'test',
    };
  }
}

Decorators reference

Signature Example Description
@controller(controllerRoute: string, ...middlewares) @controller("/product") class ProductController class defined as controller, the paths of the methods that contain this class will be concatenated.
@httpGet(methodRoute: string, ...middlewares) @httpGet("/") async getProduct() This decorator defines a route of type GET
@httpPost(methodRoute: string, ...middlewares) @httpPost("/") async createProduct() This decorator defines a route of type POST
@httpPut(methodRoute: string, ...middlewares) @httpPut("/:productId") async updateProduct() This decorator defines a route of type PUT
@httpDelete(methodRoute: string, ...middlewares) @httpDelete("/:productId") async removeProduct() This decorator defines a route of type DELETE