@reflet/express
v2.0.0
Published
Well-defined and well-typed express decorators
Downloads
1,546
Maintainers
Readme
@reflet/express
🌠
[!IMPORTANT]
Upgrade from v1 to v2 : Migration guide
The best decorators for Express. Have a look at Reflet's philosophy.
- Getting started
- Routing
- Middlewares
- Request properties injection
- Sending return value
- Error handling
- Application class
- Pure dependency injection
Getting started
Enable experimental decorators in TypeScript compiler options.No need to install "reflect-metadata".
"experimentalDecorators": true,
Install the package along with peer dependencies.
npm i @reflet/express @reflet/http express npm i -D @types/express
Create your decorated routing routers.
// thing.router.ts import { Get, Post, Res, Params, Body, Router } from '@reflet/express' @Router('/things') export class ThingRouter { @Get() async list(@Res res: Response) { const things = await db.collection('things').find({}) res.send(things) } @Get('/:id') async get(@Params('id') id: string, @Res res: Response) { const thing = await db.collection('things').find({ id }) res.send(thing) } @Post() async create(@Res res: Response, @Body body: Thing) { const newThing = await db.collection('things').insertOne(body) res.status(201).send(newThing) } }
Register them on your Express application.
// server.ts import express from 'express' import { register } from '@reflet/express' import { ThingRouter } from './thing.router.ts' const app = express() app.use(someGlobalMiddleware) register(app, [ThingRouter, /*...*/]) app.listen(3000)
The Express way
🔦
register(app, [routers])
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 router (or a controller if you prefer), 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:
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
🔦
@Route(method, path)
💫 Related Express methods:app.METHOD
,app.all
Common route decorators are created from Route
, 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, Route
is also a namespace that gives access to all route decorators as its properties.
const Options = (path?: string | RegExp) => Route('options', path)
@Router('/')
class ThingRouter {
@Options('/things')
opts(req: Request, res: Response, next: NextFunction) {}
@Route.All('/things')
all(req: Request, res: Response, next: NextFunction) {}
@Route.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 Route
.
const Patch_Put = (path: string | RegExp) => Route(['patch', 'put'], path)
class ThingRouter {
@Patch_Put('/things/:id')
update(req: Request, res: Response, next: NextFunction) {}
}
Router
🔦
@Router(path, options?)
💫 Related Express method:express.Router
You then attach routes to an Express Router, so they can share a root path, just like with plain Express.
@Router('/things')
class ThingRouter {
@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
🔦
@Router.Children(register)
You can register child routers with the dedicated decorator Router.Children
:
@Router('/album')
@Router.Children(() => [TrackRouter])
class AlbumRouter {}
@Router('/:albumId/track', { mergeParams: true })
class TrackRouter {}
Paths centralization and constraint
You might want the root paths of your routers to be centralized as well, so you can have a glance at all of them. 👀You can register your routers as a tuple with a path constraint (Reflet will enforce those paths):
@Router('/foo')
class Foo {
@Get()
list(req: Request, res: Response, next: NextFunction) {}
}
register(app, [['/foo', 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 tuple:
@Router('/decorated')
class Decorated {
@Get()
list(req: Request, res: Response, next: NextFunction) {}
}
const plain = express.Router().get('', (req, res, next) => {})
register(app, [
['/decorated', Decorated],
['/plain', 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 ItemRouter {
@Get()
list(req: Request, res: Response, next: NextFunction) {}
}
@Router('/foo')
@Router.Children(() => [['/items', ItemRouter]])
class FooRouter {}
@Router('/bar')
@Router.Children(() => [['/elements', ItemRouter]])
class BarRouter {}
Handler parameters injection
You can inject the handler parameters in any order by applying dedicated parameter decorators:
@Router('/things')
class ThingRouter {
@Get()
list(@Res res: Res, @Next next: Next) {
res.send('done')
}
@Post()
create(@Res() res: Res, @Req() req: Req) {
res.json(req.body)
}
}
You can apply them with or without invokation, how flexible is that. 😉
The decorators when used as types, are convenient references to express interfaces (so you don't need to import them).
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 ThingRouter {
@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 routers:
@Use(express.json(), express.urlencoded())
@Use(cors())
@Router('/things')
class ThingRouter {
@Use((req, res, next) => next())
@Get()
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.
Reflet respects Express flow and will apply class-scoped middlewares to the newly created Express Router:
@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).
Scoped router middlewares
🔦
@ScopedMiddlewares
Express does not isolate middlewares of routers that share the same path (related issue).
If you wish to circumvent this default behavior, add ScopedMiddlewares
decorator to a router, to scope its middlewares (and its error handlers) to its routes only.
@Router('/foo')
@ScopedMiddlewares
@Use(authenticate)
class FooSecret {
@Get()
getSecret(req: Request, res: Response, next: NextFunction) {}
}
@Router('/foo')
class FooPublic {
@Get()
getPublic(req: Request, res: Response, next: NextFunction) {}
}
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()
})
}
@Router('/things')
class ThingRouter {
@UseStatus(201)
@Post()
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 UserRouter {
// Whole params object
@Get('/users/:userId/things/:thingId')
get(@Params params: Params<'userId' | 'thingId'>) {}
// 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
@Router('/things')
class ThingRouter {
// Whole query object
@Get()
list(@Query query: Query) {}
// Specific field
@Get()
list(@Query('size') size?: string, @Query('color') color?: string) {}
}
Request body
🔦
@Body(key?)
💫 Related Express object:req.body
@Router('/things')
class ThingRouter {
// Whole body
@Patch('/:id')
update(@Body body: Partial<Thing>) {}
// Specific key
@Patch('/: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' }))
@Router('/things')
class ThingRouter {
@Post()
create(@Body body: Thing) {} // default jsonParser won't be applied again here.
}
Request headers
🔦
@Headers(header?)
💫 Related Node.js object:req.headers
@Router('/things')
class ThingRouter {
// Whole headers object
@Get()
list(@Headers headers: Headers) {}
// Specific header
@Get()
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.
Augment the union with the help of the global namespace RefletHttp
:
declare global {
namespace RefletHttp {
interface RequestHeader {
XCustom: 'x-custom'
}
}
}
@Router('/things')
class ThingRouter {
@Get()
list(@Headers('x-custom') custom: string) {}
}
Use RequestHeader
enum from @reflet/http
for better discoverability and documentation.
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)
@Router('/things')
class ThingRouter {
@Get()
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)
@Router('/things')
class ThingRouter {
@Get()
list(@CurrentUser user: User) {}
}
You can mark your custom decorator's middlewares for deduplication:
const CurrentUser = createParamDecorator(
(req) => req.user,
[{ handler: isAuthenticated, dedupe: true }]
)
With these options, on registering, Reflet won't add the implicit middlewares if they're already applied locally (on a route or router) or globally (on the app).
Comparison to deduplicate is done:
- by function reference with
dedupe: 'by-reference'
- by function name with
dedupe: 'by-name'
- by both function reference and name with
dedupe: true
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 router.
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]
},
[
{ handler: express.json(), dedupe: true },
{ handler: express.urlencoded(), dedupe: true },
]
)
@Router('/things')
class ThingRouter {
@Post()
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' }
}
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.
@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'
}
Custom handler
@Send<string>((data, { res }) => {
if (data === undefined) res.status(404)
if (data === null) res.status(204)
res.json({ name: data })
})
@Get('/me')
get() {
return 'Jeremy'
}
Share and override
Decorate a class with Send
to apply its behavior to all its methods. You override the behavior on a method level.
@Send({ json: true })
class PeopleRouter {
@Send({ json: false }) // override class send options
@Get('/me')
get() {
return 'Jeremy' // Content-Type: 'text/html'
}
}
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 ThingRouter {
@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
@Router('/things')
class ThingRouter {
@Catch((err, req, res, next) => {
res.status(400)
next(err)
})
@Get()
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.
@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 HTTPError
from @reflet/http
for an even better developer experience. Compatible with express default error handler as well.
Final Handler
🔦
finalHandler(options)
const app = express()
register(app, [ThingRouter])
app.use(finalHandler({
json: 'from-response-type',
log: '5xx',
notFoundHandler: true
}))
json
Express default error handler always sends a text/html
response (source code). This doesn't go well with today's world of JSON APIs.
json: true
always sends the error withres.json
.json: false
passes the error tonext
to be handled by express final handler (default).json: 'from-response-type'
sends the error withres.json
by looking forContent-Type
on the response:res.type('json') // ... throw Error('Nope') // Content-Type: 'application/json'
json: 'from-response-type-or-request'
first looks forContent-Type
on the response, or infers it fromX-Requested-With
orAccept
headers on the request:GET http://host/foo Accept: application/json
throw Error('Nope') // Content-Type: 'application/json'
expose
By default, Error message
and name
are not serialized to json.
With this option, you can either hide all error properties or expose some of them in the serialized response:
true
: exposes all properties (stack included, beware of information leakage !).false
: exposes nothing (empty object).string[]
: whitelists specifics properties.(status) => boolean | string[]
: function for more conditional whitelisting:
finalHandler({
json: true,
expose(status) {
// expose all properties in non production environment
if (process.env !== 'production') {
return true
}
// expose only some properties of client errors in production
if (status < 500) {
return ['message', 'code', 'data']
} else {
return false
}
}
})
log
log: true
always logs errors (withconsole.error
).log: false
never logs errors, default.log: '5xx'
only logs server errors (withconsole.error
).- If you need the flexibility to log more infos or use a dedicated logger other that
console.error
, you can pass a function like so:
import * as pino from "pino";
const logger = pino()
finalHandler({
log(err, req, res) {
logger.error({
err,
status: res.statusCode,
path: req.url,
timestamp: new Date().toISOString(),
})
},
})
The response object type only exposes safe properties, so you don't send the response by accident.
notFoundHandler
Like the error handler, Express default route handler always sends a text/html
response when the route is not found.
notFoundHandler: true
defines a default handler similar to the Express one, with a 404 status, but compatible with json.notFoundHandler: <number>
defines the same default handler, with a custom status code.notFoundHandler: (req, res, next) => {}
lets you define your own.
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 { UserRouter } from './user.router'
const app = new Application()
app.use(express.json(), express.urlencoded())
app.register([UserRouter]) // 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, Send
options, and ScopedMiddlewares
, will be shared globally !
import * as express from 'express'
import { Application, Registration, Use, Catch, Send, Router } from '@reflet/express'
import { UserRouter } from './user.router'
@Send({ json: true })
@Use(express.json(), express.urlencoded())
@Router.ScopedMiddlewares
@Catch(finalHandler({
json: true,
log: true,
notFoundHandler: true,
}))
class MyApp extends Application {
constructor(routers: Registration[]) {
super()
this.register(routers)
}
@Get('/healthcheck')
healthcheck() {
return { success: true }
}
}
const app = new MyApp([UserRouter])
app.listen(3000)
If you call register
multiple times, Reflet will make sure global middlewares are added only once, and gloral error handlers are still at the end of the stack.
Pure dependency injection
If you want to go full OOP and your routers 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 UserRouter {
constructor(private userService: IUserService) {}
@Get('/user')
async getAllUsers(@Res res: Response) {
const users = await this.userService.getUsers()
res.send(users)
}
}
register(app, [
new UserRouter(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 routers:
@Router('/parent')
@Router.Children<typeof ParentRouter>((service) => [new NestedRouter(service)])
class ParentRouter {
constructor(private service: Service) {}
}
register(app, [new ParentRouter(new Service())])