- Bootstrap the framework
- Create a controller bundle
- Add global middleware
- Set the API incoming parameters casters
- Set the error handler
- Set the logger
- Set the trace id function
- Launch the framework
- Create a controller
- Endpoint decorators
- Endpoint parameters decorators
- Defining custom endpoint variable decorators
In order to bootstrap the framework, you need to create a new Reef
class passing the express app:
const app: Express = express()
const reef = new Reef(app)
To configure a controller bundle, create a ControllerBundle
object with properties: name
for the bundle identifier, controllerDirPath
for the absolute path of the controller files, baseRoute
for the base URL path, and controllerFileNamePattern
which is a regex for matching controller filenames, supporting both .ts
and .js
files.
Example in TypeScript:
const controllerBundle: ControllerBundle = {
name: 'internal-api',
controllerDirPath: join(__dirname, 'controllers-internal-api'),
baseRoute: '/api/v1/',
controllerFileNamePattern: /(\.controller|Controller)\.(ts|js)/g,
}
reef.setControllerBundle(controllerBundle);
You can add global middleware to the express app
reef.addGlobalMiddleware(express.json())
reef.addGlobalMiddleware(express.urlencoded({extended: false}))
You can set the casters for the incoming parameters, the casters are used to cast the incoming request parameters to the desired type, e.g. if you want to cast a string to a date, you can create a caster for that
e.g. setting the casters
class MyCasters extends DefaultCasters {
Date(input: unknown) {
const timestamp = Date.parse(input as string)
if (isNaN(timestamp)) throw new this.ErrorClass('date_not_valid')
return new Date(timestamp)
}
}
reef.setCasters(MyCasters)
e.g. using the casters
@Controller('/')
export class TestController {
@Get('/get')
test(@Query() date: Date) {
return date
}
}
the date query parameter will be cast to a Date
object according to the caster class method Date
by matching the
class name of the endpoint parameter, with the caster class method name
You can set the error handler for the express app
function MyErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
res.status(500).send('Something broke!')
}
reef.setErrorHandler(MyErrorHandler)
You can set the logger for the express app. The getLogger function must return an instance of a class that implements
the GenericLogger
interface
class Logger implements GenericLogger {
debug(...messages: string[]) {
console.debug(...messages)
}
info(...messages: string[]) {
console.info(...messages)
}
warn(...messages: string[]) {
console.warn(...messages)
}
error(...messages: string[]) {
console.error(...messages)
}
}
function getLogger() {
return new Logger()
}
reef.setGetLoggerFn(getLogger)
You can set the trace id function for the express app. The trace id function must return a string
function getTraceId(req: e.Request) {
return req.header('X-Trace-Id') || uuidv4()
}
reef.setGetTraceIdFn(getTraceId)
You can launch the framework
reef.launch()
import express, {Express} from 'express'
import {Reef} from 'reef-framework'
import {join} from 'path'
import {DefaultCasters} from 'reef-framework'
import {GenericLogger} from 'reef-framework'
import {Request} from 'express'
import {Response} from 'express'
import {NextFunction} from 'express'
import {v4 as uuidv4} from 'uuid'
class MyCasters extends DefaultCasters {
Date(input: unknown) {
const timestamp = Date.parse(input as string)
if (isNaN(timestamp)) throw new this.ErrorClass('date_not_valid')
return new Date(timestamp)
}
}
class Logger implements GenericLogger {
debug(...messages: string[]) {
console.debug(...messages)
}
info(...messages: string[]) {
console.info(...messages)
}
warn(...messages: string[]) {
console.warn(...messages)
}
error(...messages: string[]) {
console.error(...messages)
}
}
function getLogger() {
return new Logger()
}
function getTraceId(req: Request) {
return req.header('X-Trace-Id') || uuidv4()
}
function MyErrorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
res.status(500).send('Something broke!')
}
const app: Express = express()
const reef = new Reef(app)
reef
.setControllerBundle(
'/api/v1/',
join(__dirname, 'controllers'),
/^.+\.controller/g,
true
).setControllerBundle(
'/api/v2/',
join(__dirname, 'controllers-v2'),
/^.+\.controller/g,
true
)
.addGlobalMiddleware(express.json())
.addGlobalMiddleware(express.urlencoded({extended: false}))
.setCasters(MyCasters)
.setGetLoggerFn(getLogger)
.setGetTraceIdFn(getTraceId)
.setErrorHandler(MyErrorHandler)
.launch()
In order to create a controller you need to create a class that extends the BaseController
class and decorate it with
the @Controller(basePath: string)
decorator
@Controller('/users')
export class MyController extends BaseController {
private users = [
{id: 1, name: 'George'},
{id: 2, name: 'John'},
{id: 3, name: 'Paul'},
{id: 4, name: 'Ringo'},
]
@Get('/')
getAllUsers() {
return this.users
}
@Get('/:id')
getUserById(@Param() id: number) {
return this.users.find(u => u.id === id)
}
@Post('/')
createUser(@Body() user: { name: string }) {
this.users.push({id: this.users.length + 1, name: user.name})
return this.users
}
}
There are 5 endpoint decorators:
@Get
, @Delete
, @Put
, @Post
and @Patch
All 5 accept 2 arguments path: string
and autoResponse: boolean
(optional) -- defaults to true
path is the uri part after the controller base path
autoResponse is a boolean that sets if the framework will automatically send the response or if the endpoint will send
the response manually
e.g.
@Controller('/my-controller')
class MyController extends BaseController {
@Get('/')
getAllUsers() {
return {success: true}
}
@Get('/manual-response', false)
getAllUsers(@Res() res: e.Response) {
res.setHeader('Content-Type', 'application/json')
.status(204)
.send({success: true})
}
}
there are 3 endpoint parameters decorators for the 3 types of parameters:
@Body
, @Query
and @Param
All 3 accept 2 arguments path?: string
(optional) -- defaults to the parameter name
and autoCast?: boolean
(optional) -- defaults to true
path is the path for the variable value in the req.body
, req.query
and req.param
respectively
in the case of the body you can use the dot notation to access nested properties
@Controller('/my-controller')
class MyController extends BaseController {
// This endpoint is called with the following request body:
// {
// "user": {
// "name": "John"
// }
// }
@Post('/')
createUser(@Body('user.name') user: string) {
console.log(user) // John
return {success: true}
}
}
autoCast
is a boolean
that sets if the framework will automatically cast the parameter to the desired type (based on the
casters that are set in the bootstrap phase)
@Controller('/my-controller')
class MyController extends BaseController {
// Both endpoints are called with the following request query:
// ?returnDeleted=true
// Even though the query parameters are always strings, if you set autoCast to true (which is the default)
// the parameter will be casted to the desired type (if a caster is defined for that type)
@Get('/users')
getUsers(@Query() returnDeleted: boolean) {
console.log(typeof returnDeleted) // "boolean"
return {success: true}
}
// Since query parameters are always strings, if you set autoCast to false,
// the parameter will be a string no matter what you pass as a query parameter type
@Get('/users2')
getUsers2(@Query(null, false) returnDeleted: boolean) {
console.log(typeof returnDeleted) // "string"
return {success: true}
}
}
There are 2 more decorators for the endpoint parameters:
@Req()
and @Res()
Both does not accept any arguments and return the req
and res
objects respectively
@Controller('/my-controller')
class MyController extends BaseController {
@Get('/users')
getUsers(@Req() req: e.Request, @Res() res: e.Response) {
console.log(req) // Request object
console.log(res) // Response object
return {success: true}
}
}
You can define your own endpoint variable decorators by using the createParamDecoratorInternal
function and passing the following arguments:
path: string
-- the path for the variable value in thereq.body
,req.query
andreq.param
respectivelyautoCast: boolean
-- aboolean
that sets if the framework will automatically cast the parameter to the desired typegetValue: (req: Request, res: Response, casters: DefaultCasters, meta: EndpointParamMeta) => unknown | Promise<unknown>
-- a function that returns the value of the variable
e.g.
export function Header(headerName?: string) {
return createParamDecorator('', false, {
getValue(req: Request, res: Response, casters: DefaultCasters, meta: EndpointParamMeta): unknown | Promise<unknown> {
return req.header(headerName || kebabCase(meta.name))
}
})
}
@Controller('/my-controller')
class MyController extends BaseController {
@Get('custom-param-decorator-test')
customParamDecoratorTest(@Header() xTestHeader: string) {
return { header: xTestHeader }
}
}
function createEndpointMiddleware(subject: symbol, params: unknown) {}
There are 2 functions that can be used to create custom middleware decorators:
createEndpointMiddleware
-- creates a middleware decorator for the endpoint functionscreateControllerMiddleware
-- creates a middleware decorator for the controller class Both functions accept 2 arguments:subject: symbol
-- the symbol that represents the middleware decoratorparams: unknown
-- the parameters that will be passed to the middleware generator class in order to return the proper middleware functions
Reef class -- in the bootstrapping phase implements a method called setMiddlewareGenerator
that accepts a class that implements the IMiddlewareGenerator
interface
IMiddlewareGenerator
interface has 2 methods:
getMiddlewareSymbols
-- this function is the first one triggered by the framework. The function should return the symbols of the custom middleware decorators in order for the framework to search for those symbols in the metadata of the controllers and the endpoints.getMiddleware
-- The framework after getting the symbols and gathering the metadata passed to the controllers and endpoints through the custom decorators, invokes this function with parameters that contain the metadata of the controllers and the endpoints. The data are passed in a form of an object that has as keys the symbols that got through thegetMiddlewareSymbols
function and as value the parameters passed to the decorators. Since a decorator can be used multiple times in the same controller or endpoint, the parameters are passed as an item in an array. The function should return an array ofRequestHandler
functions that will be used as middleware for the controllers and the endpoints.
e.g. Lets say that we want on endpoint level to set a role -- or roles -- and allow access to that endpoint only when we have that role in the header field x-role
Lets first create the custom middleware decorators for the endpoint and the class
/// file: auth.middleware.ts
import {createControllerMiddleware} from "./controller.decorator";
export enum ROLE {
USER = 'USER',
ADMIN = 'ADMIN',
}
export const authSymbol = Symbol('auth-symbol')
// endpoint decorator
export function Auth(role: ROLE) {
return createEndpointMiddleware(authSymbol, role)
}
// controller decorator
export function CAuth(role: ROLE) {
return createControllerMiddleware(authSymbol, role)
}
Now lets create the middleware generator class.
/// file: my-middleware-generator.class.ts
export type MiddlewareOptions = {
[authSymbol]: ROLE[],
}
export class MyMiddlewareGenerator implements IMiddlewareGenerator {
getMiddlewareSymbols(): symbol[] {
return [authSymbol]
}
getMiddleware(
controllerOptions: MiddlewareOptions,
endpointOptions: MiddlewareOptions
): RequestHandler[] {
// We would like to use the endpointOptions if they are defined, otherwise we will use the controllerOptions
const acceptedRoles = []
if (endpointOptions && endpointOptions[authSymbol]) {
acceptedRoles.push(...endpointOptions[authSymbol])
} else if (controllerOptions && controllerOptions[authSymbol]) {
acceptedRoles.push(...controllerOptions[authSymbol])
}
if (!acceptedRoles.length) {
return []
}
// We return an array of RequestHandler functions that will be used as middleware for the endpoints
return [(req: e.Request, res: e.Request, next: e.NextFunction) => {
const role = req.header('x-role')
if (acceptedRoles.includes(role)) {
return next()
}
return res.status(403).send('Forbidden')
}]
}
}
Now lets use the middleware generator class in the bootstrap phase
import {MyMiddlewareGenerator} from "./my-middleware-generator.class";
const app: Express = express()
const reef = new Reef(app)
reef
.setControllerBundle('/api/v1/', join(__dirname, 'controllers'), /^.+\.controller/g, true)
.addGlobalMiddleware(express.json())
.addGlobalMiddleware(express.urlencoded({extended: false}))
.setCasters(MyCasters)
.setGetLoggerFn(getLogger)
.setGetTraceIdFn(getTraceId)
.setErrorHandler(MyErrorHandler)
.setMiddlewareGenerator(MyMiddlewareGenerator)
.launch()