@bitmountain/expressive
TypeScript icon, indicating that this package has built-in type declarations

1.0.3 • Public • Published

🚀 Expressive

Simple, flexible and lightweight library for creating express routers using decorators.

npm version codecov

demo

Features

  • Create routers using @Controller (or its alias, @Router) in a class.
  • Define endpoints by annotating methods with @Get, @Post, @Put, @Delete, @All, etc. More obscure http methods can be used via @Route(<http-method>, <path>)
  • Define class or method-level middlewares using @Middleware
  • Optionally wrap your methods using @Wrapper: which makes it ideal for handling async methods!
  • Provides aliases for most commonly used http methods (e.g Get, @Post, @Put, etc) and also allows custom http methods to be used via @Route(<verb>)
  • Lightweight: depends directly only on reflect-metadata and the codebase has around 250 lines of code!
  • Can be gradually adopted: you can use it only for parts of your api, if you desire.

Installation

Install both @bitmountain/expressive and express (or express@next if you want to give express 5.x a try).

npm install @bitmountain/expressive express

Expressive is compatible with express 4.x and express 5.x (alpha).

Note that this library make heavy use of decorators - so make sure you have them enabled in your TypeScript settings:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    // other compiler options
  }
}

Getting started

import bootstrap, { Controller, Get } from '@bitmountain/expressive';
import express from 'express';

@Controller('/hello')
export class FooController {

  @Get('/world')
  public bar(req: Request, res: Response) {
    res.send('Hello world!');
  }
}

// create your express app like you normally would
const app = express();

//  the line below will create and register the routers into the app
// (you can also pass additional options or use .create to manually register the routers
// - see the rest of the documentation below)
expressive.bootstrap(app, [new FooController()])
app.listen(3000);

Decorators

Decorator Description Where can it be applied?
@Controller(basePath: string, options?: RouterOptions) Allow classes to group methods/endpoints under a common base path. An express RouterOptions object can also be provided with additional configuration Classes
@Router(basePath: string, options: RouterOptions) Alias for @Controller Classes
@Get/@Post/@Put/@Delete/@Options/@Patch/@Head(path: string | RegExp | string[] | RegExp[]) Used to convert a class method/property to an express route for serving requests matching the equivalent HTTP Verb. Methods
@Route(verb: string, path: string | RegExp | string[] | RegExp[]) Same as above, but allows you to manually specify the http verb you want to use. Methods
@All(path: string | RegExp | string[] | RegExp[]) Same as above, but matches all http verbs (see express.all()) Methods
@Middleware(RequestHandler | RequestHandler[]) Used to apply one or more middlewares to a method or a class. Middlewares applied to classes will be invoked for each method of the class. Classes and methods
@ErrorMiddleware(ErrorRequestHandler | ErrorRequestHandler[]) Used to apply one or more error middlewares to a method or a class. Error middlewares applied to classes will be invoked for each method of the class. Classes and methods
@Wrapper(wrapperFunction: () => RequestHandler) Can be applied to a method or class. When applied to a class, wraps all methods of the class with the provided function. Classes and methods

API

expressive.bootstrap(app, [controllers], options?)

Creates one express router for each controller instance provided and registers them in the given express app.

  • options:
{

  // array of middlewares which will be applied to all routes
  // middlewares can also be specified at a class/method level
  // using the @Middleware decorator
  globalMiddlewares?: []

  // wrapper function which will "wrap" each one of the routes
  // wrappers can also be specified at a class/method level using the
  // @Wrapper decorator
  globalWrapper?: (fn) => (req, res, next) => fn(req, res, next)

  // by default, expressive uses express.Router(opts) to construct
  // router instances (where the "opts" variable is optionally provided
  // via the @Controller decorator. You can use this variable if you want
  // expressive to use a custom router
  routerFactory: (opts?: RouterOptions) => Router;
}

expressive.create([controllers], options?)

Creates one express router for each one of the controller instances provided. When using this function you have to manually register the controllers in the express app, like so:

const app = express();
const cfgs = expressive.create([new UsersController()]);
cfgs.forEach(cfg => app.use(cfg.basePath, cfg.router));
  • options:
{

  // wrapper function which will "wrap" each one of the routes
  // wrappers can also be specified at a class/method level using the
  // @Wrapper decorator
  globalWrapper?: (fn) => (req, res, next) => fn(req, res, next)

  // by default, expressive uses express.Router(opts) to construct
  // router instances (where the "opts" variable is optionally provided
  // via the @Controller decorator. You can use this variable if you want
  // expressive to use a custom router
  routerFactory: (opts?: RouterOptions) => Router;
}

Examples

Applying a middleware to a single method in a controller

@Controller('/users')
export class UsersController {

  @Get('/:id')
  // the middleware below will be invoked before findById is executed
  @Middleware([loggingMiddleware()])
  public findById(req: Request, res: Response) {
    res.json({ foo: 'bar' });
  }
}

Applying a middleware to all methods in a controller

@Controller('/users')
// the middleware below will be executed for all methods in this controller
@Middleware([authMiddleware()])
export class UsersController {

  @Get('/:id')
  public findById(req: Request, res: Response) {
    res.json({ foo: 'bar' });
  }

  @Get('/')
  public findAll(req: Request, res: Response) {
    res.send([{ user: '' ]);
  }
}

Adding a middleware to all methods in ALL controllers

@Controller('/users')
export class UsersController {  /* methods here */ }

@Controller('/projects')
export class ProjectsController {  /* methods here */ }

const app = express();
expressive.bootstrap(app, [new UsersController(), new ProjectsController()], {
  // the middleware below will be added to all methods in all controllers registered above
  // you can also add middlewares at controller or method level using the @Middleware decorator
  globalMiddlewares: [loggingMiddleware()]
})

Handling async methods

One way to handle async methods is to wrap the functions using an async handler. You can wrap all the methods in a controller using @Wrapper or provide a value to the globalWrapper property when calling expressive.bootstrap:

Note: This is only needed for express 4.x or below. Starting from express 5.x an async wrapper will no longer be needed, as express automatically takes care of handlers that return promises.

import asyncHandler from 'express-async-handler';

@Controller('/users')
@Wrapper(asyncHandler)
export class UsersController {

  @Get('/:id')
  public async findById(req: Request, res: Response) {
    const user = await db.findUserById(req.params.id);
    res.json(user);
  }
}

const app = express();
express.bootstrap(app, [new UsersController()]);

// alternatively, you can get rid of the @Wrapper decorator above
// and apply it to all controllers at once:
// express.bootstrap(app, [new UsersController()], {
//  globalWrapper: asyncHandler
//});

FAQ

- In what order are routes matched?

If a request match 2 different routes, only the first matched route (the one the appears first in the class) will be executed. This matches express' behaviours, where the first route always takes priority. As an example:

@Controller('/users')
export class UsersController {

  @Get('/:id')
  public foo() {}

  // IMPORTANT: this will never be executed, as the route above (GET /:id) was
  // registered first and also matches the endpoint GET /users/hello
  @Get('/hello')
  public hello() {}
}

- In what order are middlewares executed?

In the order they were provided (just like in express)

// execution order goes from left to right
@Middleware([first(), second(), third()])
@Get('/:id')
public loadById(req: Request, res: Response) {

}

- How do I handle async methods?

If using express 5.x (in alpha as of this writing) you don't have to do anything, as async routes are automatically handled by express. If using express <= 4.x you need to either add a try/catch block around your async methods (and call next() accordingly) or use an async wrapper. You can write your own async wrapper in a few lines of code or use a library such as express-async-handler.

There's one example above in this documentation that covers how to use an async wrapper.

Why expressive?

  • There are other libraries/frameworks out there that serve a similar purpose (NestJS, tsed, Overnight, inversify-express-utils), but they either have a much steeper learning curve or they are too opinionated in the way you create your app and controllers. Expressive just provides syntactic sugar so you can create and register the routers in a more elegant way.

License

MIT

Package Sidebar

Install

npm i @bitmountain/expressive

Weekly Downloads

0

Version

1.0.3

License

MIT

Unpacked Size

42.9 kB

Total Files

40

Last publish

Collaborators

  • marlonbernardes