npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@bitmountain/expressive

v1.0.3

Published

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

Downloads

14

Readme

🚀 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