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

2.0.0 • Public • Published

@reflet/express-middlewares 🧩

lines coverage statements coverage functions coverage branches coverage

[!IMPORTANT]
Upgrade from v1 to v2 : Migration guide

Convenient middleware decorators to use with Reflet/express.

Getting started

  1. Get started with Reflet/express.

  2. Install the package.

    npm i @reflet/express-middlewares
  3. Apply middleware decorators on routers or specific routes.

Request authorizations

🔦 @UseGuards(...guards)

A guard is a filter function that takes the Request object as parameter and should return a boolean (asynchronously or not) to authorize the request or not.

@Router('/things')
class ThingRouter {
  @UseGuards(
    (req) => req.user != null,
    (req) => req.user.admin === true,
  )
  @Get('/secret')
  get() {}
}

If the guard returns...

  • true: The request will be processed.
  • false: The request will be denied with a 403 HTTP status and a "Access Denied" message.

Custom error message

If you want to override the default "Access denied" message, you can return (not throw) an Error instance instead of just false.

@UseGuards((req) => Boolean(req.user.admin) || Error('You must be admin'))

🗣️ Again, be sure to return the Error and not throw it, unless you want it to be handled elsewhere.

Response interception

🔦 @UseInterceptor(mapper)

Intercept and manipulate the response body before it is sent, with a mapping function (asynchronous or not).

@Router('/things')
class ThingRouter {
  @UseInterceptor<{ foo: number }>((data) => ({ foo: data.foo * 5 }))
  @Get('/things')
  list(@Res res: Response) {
    res.send({ foo: 1 }) // expect { foo: 5 }
  }

  // Gives also access to Request and Response objects:
  @UseInterceptor<{ foo: number }>((data, context) => ({ foo: context.res.statusCode }))
  @Get('/things/:id')
  get(@Res res: Response) {
    res.send({ foo: 1 }) // expect { foo: 200 }
  }

  // You can add a different constraint on the return shape:
  @UseInterceptor<{ foo: number }, { foo: string }>((data) => ({ foo: data.foo.toString() }))
  @Get('/things/:id')
  get(@Res res: Response) {
    res.send({ foo: 1 }) // expect { foo: "1" }
  }
}

About errors

UseInterceptor won't intercept errors, whether:

  • the response body is an Error instance,
  • the response has an error status (>=400).

If you need to intercept errors as well, you should simply add a @Catch decorator:

@Catch((err, req, res, next) => { /* intercept errors */ })
@UseInterceptor((data, context) => { /* intercept success */ })

🗣️ Don't forget to set a proper error status in your error handler, or else the error body will actually be intercepted.

About streams

UseInterceptor won't intercept streaming responses either (e.g. files sent with res.sendFile or res.download).
Instead you should just use a transform stream:

createReadStream('path/to/file').pipe(transform).pipe(res)

In fact, it won't intercept any response sent by chunks with the res.write native method.

Response side-effect

🔦 @UseOnFinish(sideEffect, exposeResponseBody?)
💫 Related Node.js event: finish

You need to trigger side-effects ? Log details, send an email, etc.
UseOnFinish helps you define a callback on each response, on the finish event to be exact.

@Router('/things')
class ThingRouter {
  @UseOnFinish((req, res) => {
    console.log('Request:', req.method, req.originalUrl, req.body)
    console.log('Response:', res.statusCode)
  })
  @Post('/things')
  create() {}
}

As a safety net, any exception happening in your callback will be caught and logged to stderr instead of crashing the server. You don't want the latter for a side-effect. This does not exempt you to properly handle your errors, though.

Retrieve the response body

You can expose the response body on the Response object by switching the last parameter on. Streaming responses will have their body truncated to the first 64kb, to avoid eating up memory.

@Router('/things')
class ThingRouter {
  @UseOnFinish((req, res) => {
    console.log('Request:', req.method, req.originalUrl)
    console.log('Response:', res.statusCode, res.body)
  }, true)
  @Get('/things')
  list() {}
}

Response status

🔦 @UseStatus(code)
💫 Related Express method: res.status

Set the response status code with a dedicated decorator.

@Router('/things')
class ThingRouter {
  @UseStatus(201)
  @Post('/things')
  create() {}
}

UseStatus input type is narrowed to a union of known 1XX, 2XX and 3XX status codes (instead of just number).

Use Status enum from @reflet/http for better discoverability and documentation.

Response headers

Set headers

🔦 @UseResponseHeader(header, value)|(headers)
💫 Related Express method: res.set

Set any response header with a dedicated decorator.

@UseResponseHeader({ 'x-powered-by': 'brainfuck' })
@Router('/things')
class ThingRouter {
  @UseResponseHeader('allow', 'GET')
  @Post('/things')
  create(@Res res: Response) {
    res.sendStatus(405)
  }
}

UseResponseHeader input type is narrowed to a union of known response headers (instead of just string).

Augment the union with the help of the global namespace RefletHttp:

declare global {
  namespace RefletHttp {
    interface ResponseHeader {
      XCustom: 'x-custom'
    }
  }
}

@Router('/things')
class ThingRouter {
  @Get()
  list() {}
}

Use ResponseHeader enum from @reflet/http for better discoverability and documentation.

Append header value

🔦 @UseResponseHeader.Append(header, value)
💫 Related Express method: res.append

Appends the specified value to the HTTP response header field. If the header is not already set, it creates the header with the specified value.

@UseResponseHeader.Append('link', ['<http://localhost/>', '<http://localhost:3000/>'])

Response type

🔦 @UseType(contentType)
💫 Related Express method: res.type

Set the response Content-Type with a dedicated decorator.

@Send()
@Router('/things')
class ThingRouter {
  @UseType('docx')
  @Get('/document')
  download() {
    return createReadStream('path/to/doc')
  }
}

UseType input type is narrowed to a union of common MIME types and extensions (instead of just string). You can still opt-out by expanding the input type to string or any:

@UseType<string>('application/x-my-type')

Conditional middleware

🔦 @UseIf(filter, [middlewares])

In some case, you might want to separate a middleware logic from its applying conditions. UseIf is made for such case.

UseIf((req) => req.method === 'POST', [express.json()])
@Router('/things')
class ThingRouter {}

Package Sidebar

Install

npm i @reflet/express-middlewares

Weekly Downloads

11

Version

2.0.0

License

MIT

Unpacked Size

38.1 kB

Total Files

13

Last publish

Collaborators

  • jeben