Naked Panda Meditations

    @reflet/express
    TypeScript icon, indicating that this package has built-in type declarations

    1.6.1 • Public • Published

    @reflet/express 🌠

    lines coverage statements coverage functions coverage branches coverage

    The best decorators for Express. Have a look at Reflet's philosophy.

    Getting started

    1. Make sure you have decorators enabled. (click for details)

      • Enable them in your TypeScript compiler options.

        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
      • Install reflect-metadata shim.

        yarn add reflect-metadata
      • Import the shim in your program before everything else.

        import 'reflect-metadata'
    2. Install the package along with peer dependencies.

      yarn add @reflet/express express && yarn add -D @types/express @types/node
    3. Create your decorated routing controllers.

      // thing.controller.ts
      import { Get, Post, Res, Params, Body } from '@reflet/express'
      
      export class ThingController {
        @Get('/things')
        async list(@Res res: Response) {
          const things = await db.collection('things').find({})
          res.send(things)
        }
      
        @Get('/things/:id')
        async get(@Params('id') id: string, @Res res: Response) {
          const thing = await db.collection('things').find({ id })
          res.send(thing)
        }
      
        @Post('/things')
        async create(@Res res: Response, @Body body: Thing) {
          const newThing = await db.collection('things').insertOne(body)
          res.status(201).send(newThing)
        }
      }
    4. Register them on your Express application.

      // server.ts
      import 'reflect-metadata'
      import express from 'express'
      import { register } from '@reflet/express'
      import { ThingController } from './thing.controller.ts'
      
      const app = express()
      app.use(someGlobalMiddleware)
      
      register(app, [ThingController, /*...*/])
      
      app.listen(3000)

    The Express way

    🔦 register(app, [controllers])

    As you can see, the main method register simply accepts an Express app and an array of your classes.

    You still apply your global middlewares and start your server in the Express way you already know. This means you can progressively add Reflet to your existing app. 😉

    If you have a more complex bootstraping, reflet allows you to inherit the express original application with Application class.

    Routing

    To handle requests with a class, let's call it a controller, you simply have to decorate its methods with route decorators.

    Common route decorators

    🔦 @Get(path), @Post(path), @Patch(path), @Put(path), @Delete(path)
    💫 Related Express methods: app.get, app.post, app.put, app.delete

    Reflet directly exposes common route decorators handling the majority of routing use cases. Here is a comparaison of Reflet and plain Express for basic requests:

    HTTP request Reflet Express
    GET http://host/foo
    @Get('/foo')
    get(req, res, next) {}
    app.get('/foo', (req, res, next) => {})
    POST http://host/foo
    @Post('/foo')
    create(req, res, next) {}
    app.post('/foo', (req, res, next) => {})
    PATCH http://host/foo
    @Patch('/foo')
    update(req, res, next) {}
    app.patch('/foo', (req, res, next) => {})
    PUT http://host/foo
    @Put('/foo')
    replace(req, res, next) {}
    app.put('/foo', (req, res, next) => {})
    DELETE http://host/foo
    @Delete('/foo')
    remove(req, res, next) {}
    app.delete('/foo', (req, res, next) => {})

    Pretty obvious, like any other decorator framework.

    Other route decorators

    🔦 @Method(method, path)
    💫 Related Express methods: app.METHOD, app.all

    Common route decorators are created from Method, a decorator in itself, that can be used to create a route decorator for any other routing method supported by Express (plus the all method).

    As a convenience, Method is also a namespace that gives access to all route decorators as its properties.

    const Options = (path: string | RegExp) => Method('options', path)
    
    class Controller {
      @Options('/things')
      opts(req: Request, res: Response, next: NextFunction) {}
    
      @Method.All('/things')
      all(req: Request, res: Response, next: NextFunction) {}
    
      @Method.Get('/things')
      get(req: Request, res: Response, next: NextFunction) {}
    }

    Handler with multiple verbs

    You can share the same handler with multiple HTTP verbs, by passing an array to Method.

    const Patch_Put = (path: string | RegExp) => Method(['patch', 'put'], path)
    
    class Controller {
      @Patch_Put('/things/:id')
      update(req: Request, res: Response, next: NextFunction) {}
    }

    Router

    🔦 @Router(path, options?)
    💫 Related Express method: express.Router

    As you've seen, you don't need to decorate your controller classes for routing to work (contrary to most frameworks). The routes will simply be attached to the main app.

    But you can also attach routes to an Express Router, so they can share a root path, just like with plain Express.

    @Router('/things')
    class Controller {
      @Get()
      list(req: Request, res: Response, next: NextFunction) {}
    
      @Get('/:id')
      get(req: Request, res: Response, next: NextFunction) {}
    
      @Post('/:id')
      create(req: Request, res: Response, next: NextFunction) {}
    }

    Express Router options can be defined as a second argument:

    @Router('/things', { strict: true, caseSensitive: true })

    🗣️ Beware of VSCode auto-import, it will first try to import Router from Express instead of Reflet.

    Nested routers

    🔦 register(parent, [controllers]) 🔦 @Router.Children(register)

    You can register child routers within the constructor of the parent router, using the same register function.

    @Router('/album')
    class AlbumController {
      constructor() {
        // ⚠️ Should always be used within the constructor of a `@Router` decorated class.
        register(this, [TrackController])
      }
    }
    
    @Router('/:albumId/track', { mergeParams: true })
    class TrackController {}

    As a more declarative alternative, you can register child routers with the dedicated decorator Router.Children:

    @Router('/album')
    @Router.Children(() => [TrackController])
    class AlbumController {}
    
    @Router('/:albumId/track', { mergeParams: true })
    class TrackController {}

    Paths centralization and constraint

    You might want the root paths of your controllers to be centralized as well, so you can have a glance at all of them. 👀
    You can register all your controllers with a path constraint (Reflet will enforce those paths):

    @Router('/foo')
    class Foo {
      @Get()
      list(req: Request, res: Response, next: NextFunction) {}
    }
    
    register(app, [{ path: '/foo', router: Foo }])

    Also possible with child routers.

    Plain express routers

    To be able to progressively switch to Reflet, you can still register your plain express routers, with the help of the previous path property:

    @Router('/decorated')
    class Decorated {
      @Get()
      list(req: Request, res: Response, next: NextFunction) {}
    }
    
    const plain = express.Router().get('', (req, res, next) => {})
    
    register(app, [
      { path: '/decorated', router: Decorated },
      { path: '/plain', router: plain }
    ])

    Also possible with child routers.

    Dynamic nested routers

    🔦 Router.Dynamic(options?)

    A dynamic router is a router without a predefined path. Its path is then defined at registration.

    Useful if you need to share a child router with multiple parents, and attach it on different paths.

    @Router.Dynamic()
    class Items {
      @Get()
      list(req: Request, res: Response, next: NextFunction) {}
    }
    
    @Router('/foo')
    class Foo {
      constructor() {
        register(this, [{ path: '/items', router: Items }])
      }
    }
    
    @Router('/bar')
    class Bar {
      constructor() {
        register(this, [{ path: '/elements', router: Items }])
      }
    }

    Handler parameters injection

    🔦 @Req, @Res, @Next
    💫 Related Express objects: req, res

    You can inject the handler parameters in any order by applying dedicated parameter decorators:

    class Controller {
      @Get('/things')
      list(@Res res: Response, @Next next: NextFunction) {
        res.send('done')
      }
    
      @Post('/things')
      create(@Res() res: Response, @Req() req: Request) {
        res.json(req.body)
      }
    }

    You can apply them with or without invokation, how flexible is that. 😉

    Looking for other decorators like @Body ? Request properties injection.

    Async support

    Async functions (routes and middlewares) are properly wrapped to pass errors on to next and to the express error handling system.

    class Controller {
      @Get('/thing')
      async get() {
        await Promise.reject('oops') // properly handled by next callback: next('oops')
      }
    }

    Middlewares

    🔦 @Use(...middlewares)
    💫 Related Express method: app.use

    Apply middlewares on specific routes or whole controllers:

    @Use(express.json(), express.urlencoded())
    @Use(cors())
    class Controller {
      @Use((req, res, next) => next())
      @Get('/things')
      list() {}
    }

    Use is highly versatile, like the underlying app.use method. You can pass as many middlewares as you want inside a Use decorator, and you can apply as many Use decorators as you want on a single class or method.

    If Router decorator is used, Reflet will apply class-scoped middlewares to the newly created Express Router.

    Reflet Express equivalent
    @Use(A)
    class Foo {
      @Use(B, C)
      @Use(D)
      @Get('/foo')
      get(req, res, next) {}
    }
    app.get('/foo', A, B, C, D, (req, res, next) => {})
    @Use(A)
    @Use(B, C)
    @Router('/foo')
    class Foo {
      @Use(D)
      @Get()
      get(req, res, next) {}
    }
    const router = express.Router()
    router.use(A, B, C)
    router.get('', D, (req, res, next) => {})
    app.use('/foo', router)
    About order

    Successive Use will be applied in the order they are written, even though decorator functions in JS are executed in a bottom-up way (due to their wrapping nature).

    Create your own middleware decorator 🔧

    The versatility of Use allows for powerful extension.

    function UseStatus(statusCode: number) {
      return Use((req, res, next) => {
        res.status(statusCode)
        next()
      })
    }
    
    class Controller {
      @UseStatus(201)
      @Post('/things')
      create(req: Request, res: Response, next: NextFunction) {}
    }

    🗣️ As a naming convention, custom middleware decorators' name should begin with Use.

    Little extra 🧩

    Before you go and copy the code above... Reflet makes full use of, well, Use and provides an add-on module for convenient middleware decorators: Reflet/express-middlewares

    Here's a list of them:

    • UseGuards for request authorization handling.
    • UseInterceptor for response body manipulation.
    • UseOnFinish for response side effects.
    • UseStatus for response status.
    • UseSet for response headers.
    • UseType for response content-type.
    • UseIf for conditional middlewares.

    Convinced yet ? Go over to the doc.

    Request properties injection

    Directly inject Request properties (and even their sub-properties) in handler parameters. Just like with Req, Res or Next, invokation is optional.

    Route params

    🔦 @Params(name?)
    💫 Related Express object: req.params

    class Controller {
      // Whole params object
      @Get('/users/:userId/things/:thingId')
      get(@Params params: { userId: string; bookId: string }) {}
    
      // Specific name
      @Get('/users/:userId/things/:thingId')
      get(@Params('userId') userId: string, @Params('thingId') thingId: string) {}
    }

    Query string

    🔦 @Query(field?)
    💫 Related Express object: req.query

    Given the request: GET http://host/things?size=large&color=green

    class Controller {
      // Whole query object
      @Get('/things')
      list(@Query query: { size?: string; color?: string }) {}
    
      // Specific field
      @Get('/things')
      list(@Query('size') size?: string, @Query('color') color?: string) {}
    }

    Request body

    🔦 @Body(key?)
    💫 Related Express object: req.body

    class Controller {
      // Whole body
      @Patch('/things/:id')
      update(@Body body: Partial<Thing>) {}
    
      // Specific key
      @Patch('/things/:id')
      update(@Body<Thing>('name') name: string) {}
    }

    Body will automatically apply the following Express body parsers on the routes using it:

    • express.json()
    • express.urlencoded({ extended: true })

    You can Use the same body parsers (or apply them globally on your app) with different options and they will take precedence:

    @Use(express.json({ limit: '500kb' }))
    class Controller {
      @Post('/things')
      create(@Body body: Thing) {} // default jsonParser won't be applied again here.
    }

    Request headers

    🔦 @Headers(header?)
    💫 Related Node.js object: req.headers

    class Controller {
      // Whole headers object
      @Get('/things')
      list(@Headers headers: IncomingHttpHeaders) {}
      
      // Specific header
      @Get('/things')
      list(@Headers('user-agent') userAgent: string) {}
    }

    Header input type is narrowed to a union of known request headers (instead of just string), so typos are prevented and you have that sweet auto-completion. You can still opt-out by expanding the input type to string or any:

    class Controller {
      @Get('/things')
      list(@Headers<string>('x-custom') custom: string) {}
    }
    💡 Tip

    Use HTTP request headers enum from the same maintainer (that would be me) for an even better developer experience.

    Create your own parameter decorator 🔧

    🔦 createParamDecorator(requestMapper, [middlewares]?, deduplicateMiddlewares?)

    Inject and manipulate whatever you need from the Request object:

    const CurrentUser = createParamDecorator((req) => req.user)
    
    class Controller {
      @Get('/things')
      list(@CurrentUser user: User) {}
    }

    Add implicit middlewares

    If your decorator needs any middleware, to work as is, Reflet got you covered:

    const isAuthenticated: RequestHandler = (req, res, next) => {
      // validate and attach user to req...
      next()
    }
    
    const CurrentUser = createParamDecorator((req) => req.user, [isAuthenticated])

    Now what if this implicit middleware is already applied explicitely before ? You might not want it to be executed twice:

    @Use(isAuthenticated)
    class Controller {
      @Get('/things')
      list(@CurrentUser user: User) {}
    }

    A final option marks your custom decorator's middlewares for deduplication:

    const CurrentUser = createParamDecorator((req) => req.user, [isAuthenticated], true)

    With this option turned on, on registering, Reflet won't add the implicit middlewares if they're already applied locally (on a route or controller) or globally (on the app). Comparison is done by reference and by name.

    That's basically how the Body decorator works with its body parsers.

    This mecanism is really powerful 🦾 and allows your custom decorator to be decoupled yet still integrate nicely within any controller.

    Example with input

    const BodyTrimmed = (key: string) => createParamDecorator(
      (req) => {
        if (typeof req.body[key] === 'string') return req.body[key].trim()
        else return req.body[key]
      },
      [express.json(), express.urlencoded()],
      true
    )
    
    class Controller {
      @Post('/things')
      create(@BodyTrimmed('name') name: string) {}
    }

    Sending return value

    🔦 @Send(options?)
    💫 Related Express method: res.send

    You want your methods' return value to be handled for you ?
    Then simply tell Reflet to Send it.

    @Send()
    @Get('/me')
    get() {
      return { name: 'Jeremy' }
    }

    By the way, you can still use the Response object to send your data, and Reflet will figure that it has already been sent. 😉

    Async and stream support

    • Promises are resolved before being sent.
    • Readable streams are piped into the response.
    Reflet Express equivalent
    @Send()
    @Get('/')
    get() {
      return Promise.resolve('done')
    }
    app.get('/', (req, res, next) => {
      Promise.resolve('done').then(value => res.send(value))
    })
    @Send()
    @Get('/')
    get() {
      return createReadStream('path/to/file')
    }
    app.get('/', (req, res, next) => {
      createReadStream('path/to/file').pipe(res)
    })

    Force JSON response

    🔦 @Send({ json: true })
    💫 Related Express method: res.json

    Behind the scene Send uses, you've guessed it, the res.send Express method. It already sends a proper JSON response for Objects and Arrays, but you might want to force JSON for any type with the help of res.json:

    @Send({ json: true }) // will use res.json behind the scene
    @Get('/me')
    get() {
      return 'Jeremy' // Content-Type: 'application/json'
    }

    Change response status

    🔦 @Send({ status: XXX, undefinedStatus: XXX, nullStatus: XXX })
    💫 Related Express method: res.status

    By default in Node.js, HTTP response status code is set to 200. To set another code:

    @Send({ status: 201 })
    @Post('/me')
    create() {
      return { name: 'Jeremy' } // 201 status
    }

    You can conditionaly set status for undefined and null values:

    @Send({ nullStatus: 205, undefinedStatus: 404 })
    @Get('/things')
    list() {
      if (conditionA) return // 404 status
      if (conditionB) return null // 205 status
      return {} // 200 status
    }
    💡 Tip

    Use HTTP status enums from the same maintainer (me again) for an even better developer experience.

    Share and override

    Decorate classes with specific Send options so they act as a base for methods' Send options.

    @Send({ json: true, status: 100 })
    class PeopleController {
      @Send({ status: 200 }) // extends class send options
      @Get('/me')
      get() {
        return 'Jeremy' // 200 status, Content-Type: 'application/json'
      }
    }

    Make exceptions

    🔦 @Send.Dont()

    You need to take full control back in one of your methods ? Apply Send.Dont to exclude a method from Send behavior.

    @Send()
    @Router('/things')
    class Controller {
      @Get()
      list() {
        return db.collection('things').find({})
      }
    
      @Send.Dont()
      @Post()
      create(@Res res: Response) {
        res.write('complex')
        res.end('stuff')
      }
    }

    Why opt-in and not default

    Other frameworks choose to handle and send the return value by default. Reflet chooses not to.

    It's not that Reflet dislikes magic. But magic should be explicit and have its own decorator.
    Magic should be under control 🧙‍, that's the reason for the Send decorator.

    Error handling

    Local error handler

    🔦 @Catch(errorHandler)
    💫 Related Express method: app.use

    class Controller {
      @Catch((err, req, res, next) => {
        res.status(400)
        next(err)
      })
      @Get('/things')
      list(req: Request, res: Response, next: NextFunction) {
        throw Error('Nope') // or next('Nope')
      }
    }

    If Router decorator is used, Reflet will apply class-scoped error handlers to the newly created Express Router.

    Reflet Express equivalent
    @Catch(A)
    class Foo {
      @Catch(B)
      @Catch(C)
      @Get('/foo')
      get(req, res, next) {
        throw Error()
      }
    }
    app.get('/foo', (req, res, next) => { throw Error() }, B, C, A)
    @Catch(A)
    @Router('/foo')
    class Foo {
      @Catch(B)
      @Catch(C)
      @Get()
      get(req, res, next) {
        throw Error()
      }
    }
    const router = express.Router()
    router.get('', (req, res, next) => { throw Error() }, B, C)
    router.use(A)
    app.use('/foo', router)
    About order

    Logically, class-scoped error handlers are applied further down the handlers' stack than method-scoped error handlers.
    And like with Use, successive Catch will be applied in the order they are written.

    💡 Tip

    Throw some HTTP Errors from the same maintainer (you know who) for an even better developer experience. Compatible with express default error handler as well.

    Global error handler

    💫 Related Express concept: Default error handler

    Reflet already adds a smart global error handler to your app. It tries to stay generic enough to not get in your way, but you can easily overwrite it by applying yours.

    JSON detection

    Express default error handler always send a text/html response (source code). This doesn't go well with today's world of JSON APIs. Reflet adds an intermediary default error handler to detect whether to send the error as a JSON response.

    • It first looks for Content-Type on the response:

      res.type('json')
      // ...
      throw Error('Nope') // Content-Type: 'application/json'
    • If none is found, it infers it from X-Requested-With or Accept headers on the request:

      GET http://host/foo
      Accept: application/json
      throw Error('Nope') // Content-Type: 'application/json'

    Status detection

    Reflet parses the error value and looks for a 4xx or 5xx status code.

    The following will set the response status to 400:

    throw { status: 400, message: 'Please stop' } // no error stack
    
    const err = Error('Please stop')
    err.status = 400
    throw err // the Express way

    If none is found, it will look at the response status code, and if still nothing, will set 500 as default.

    Error logging

    If the error has a 5xx status code, Reflet will also log it to stderr.

    Simply the default

    Don't want all that smartness ? 😥 After registering your controllers, attach your own global error handler and Reflet's global error handler will be removed from handlers' stack.

    const app = register(express(), [Controller])
    // From there, a little magic detects a new global error handler on the app.
    
    app.use((err, req, res, next) => res.status(500).send(err))
    // Your error handler has been applied and has removed Reflet's error handler.
    
    app.listen(3000)

    Application class

    🔦 Application

    Have you ever tried to turn express() into a proper class ? Reflet did. 😁

    import * as express from 'express'
    import { Application } from '@reflet/express'
    import { UserController } from './user.controller'
    
    const app = new Application()
    
    app.use(express.json(), express.urlencoded())
    app.register([UserController]) // register is now a method !
    
    app.listen(3000)

    Not much for now, but you can extend this class and use all the decorators, as if they were global : Routes will be attached at the root, and middlewares, error handlers, and Send options, will be shared globally !

    import * as express from 'express'
    import { Application, Controllers } from '@reflet/express'
    import { UserController } from './user.controller'
    
    @Send({ json: true })
    @Catch((err, req, res, next) => {
      console.error(err)
      res.status(err.status || 500)
      res.json(err)
    })
    @Use(express.json(), express.urlencoded())
    class MyApp extends Application {
      constructor(controllers: Controllers) {
        super()
        this.register(controllers)
      }
    
      @Get('/healthcheck')
      healthcheck() {
        return { success: true }
      }
    }
    
    const app = new MyApp([UserController])
    
    app.listen(3000)

    Pure dependency injection

    If you want to go full OOP and your controllers have constructor dependencies, Reflet will enforce passing them as instances (along with their dependencies) instead of classes, to the register function which then acts as a Composition Root.

    interface IUserService {
      getUsers(): Promise<User[]>
    }
    
    class UserService implements IUserService {
      async getUsers() {
        return db.collection('users').find({})
      }
    }
    
    class UserController {
      constructor(private userService: IUserService) {}
    
      @Get('/user')
      async getAllUsers(@Res res: Response) {
        const users = await this.userService.getUsers()
        res.send(users)
      }
    }
    
    register(app, [
      new UserController(new UserService())
    ])

    No DI Container magic, no cumbersome @Inject decorator 😵... Only pure DI, which is the simplest and the most strongly typed DI.

    You can even pass dependencies down your nested controllers:

    @Router('/parent')
    @Router.Children<typeof ParentController>((service) => [new NestedController(service)])
    class ParentController {
      constructor(private service: Service) {}
    }
    
    register(app, [new ParentController(new Service())])

    Install

    npm i @reflet/express

    DownloadsWeekly Downloads

    433

    Version

    1.6.1

    License

    MIT

    Unpacked Size

    127 kB

    Total Files

    21

    Last publish

    Collaborators

    • jeben