Sets up controllers, which are running with Express.js.
Execute the following command from your project folder, where your package.json
file is stored:
npm install --save @egodigital/express-controllers
Example code can be found in express-controllers-samples repository.
# install modules
npm install
# build
npm run build
You have to enable decorator feature in your tsconfig.json
file:
{
"compilerOptions": {
// ...
"experimentalDecorators": true
},
// ...
}
First create a root directory, where all your controllers will be stored and implemented, lets say /controllers
.
Then create a /controllers/index.ts
(if using TypeScript) and implement an exported / public Controller
class with the following skeleton:
import { Request, Response } from 'express';
import { ControllerBase, GET, POST, schema } from '@egodigital/express-controllers';
interface INewUser {
email?: string;
password: string;
username: string;
}
const NEW_USER_SCHEMA = schema.object({
email: schema.string()
.optional(),
password: schema.string()
.min(1)
.required(),
username: schema.string()
.required(),
});
/**
* /controllers/index.ts
*
* Base path: '/'
*/
export class Controller extends ControllerBase {
/**
* [GET] / endpoint
*
* 'index' will mapped to realtive '/' path by default.
*/
@GET()
public async index(req: Request, res: Response) {
return res.status(200)
.send('Hello, e.GO!');
}
/**
* [GET] /foo endpoint
*
* Other method names than 'index', will always be mapped
* to realtive '/{METHOD_NAME}' path
*/
@GET()
public async foo(req: Request, res: Response) {
return res.status(200)
.send('Hello, foo!');
}
/**
* [POST] /foo/:foo_id endpoint
*
* Define relative path explicitly.
*/
@POST('/foo/:foo_id')
public async foo_with_post(req: Request, res: Response) {
return res.status(200)
.send('Hello, foo.POST: ' + req.params['foo_id']);
}
/**
* [POST] /users endpoint
*
* Check JSON input via joi schema.
*/
@POST({
path: '/users',
schema: NEW_USER_SCHEMA,
})
public async create_new_user(req: Request, res: Response) {
const NEW_USER: INewUser = req.body;
// TODO ...
return res.status(200)
.send('Created new user: ' + NEW_USER.username);
}
}
For loading and initializing the controllers from /controllers
, simply create a /index.ts
file and use the following code snippet:
import * as express from 'express';
import { initControllers } from '@egodigital/express-controllers';
const app = express();
initControllers({
app,
cwd: __dirname + '/controllers',
files: ['**/*.ts', '**/*.js']
});
app.listen(8080, () => {
// server now running
});
The library will scan the complete /controllers
folder structure and map the endpoints by that structure.
You can also use other filenames as index.ts
. For example, if you would like to implement a /foo/bar
endpoint, create a /controllers/foo/bar.ts
and use the following snippet:
import { Request, Response } from 'express';
import { ControllerBase, GET } from '@egodigital/express-controllers';
/**
* /controllers/foo/bar.ts
*
* Base path: '/foo/bar'
*/
export class Controller extends ControllerBase {
/**
* [GET] /foo/bar endpoint
*/
@GET()
public async index(req: Request, res: Response) {
// TODO
}
/**
* [GET] /foo/bar/xyz endpoint
*/
@GET()
public async xyz(req: Request, res: Response) {
// TODO
}
/**
* [GET] /foo/bar/tm endpoint
*/
@GET('/tm')
public async xyz(req: Request, res: Response) {
// TODO
}
}
import { Request, Response } from 'express';
import { ControllerBase, GET, ResponseSerializerContext } from '@egodigital/express-controllers';
/**
* /controllers/index.ts
*
* Base path: '/'
*/
export class Controller extends ControllerBase {
// serialize the results of any
// controller route method and
// send each as response
public async __serialize(context: ResponseSerializerContext) {
return context.response
.header('Content-Type', 'application/json')
.send(JSON.stringify(
context.result // result of 'index()', e.g.
));
}
/**
* [GET] / relative endpoint
*/
@GET()
public async index(req: Request, res: Response) {
// this object is serialized and
// send by '__serialize()' (s. above)
return {
success: true,
data: {
'TM': '1979-09-05 23:09'
},
};
}
}
import * as express from 'express';
import { ControllerBase, POST } from '@egodigital/express-controllers';
interface INewUser {
email?: string;
password: string;
username: string;
}
/**
* /controllers/index.ts
*
* Base path: '/'
*/
export class Controller extends ControllerBase {
// define one or more middlewares
// for each route endpoint
public __use = [
express.urlencoded({ extended: true }),
];
/**
* [POST] /users endpoint
*/
@POST('/users')
public async new_user(req: express.Request, res: express.Response) {
const NEW_USER: INewUser = req.body;
// TODO ...
return res.status(200)
.send('Created new user: ' + JSON.stringify(
NEW_USER, null, 2
));
}
}
import * as express from 'express';
import { ControllerBase, GET, RequestErrorHandlerContext } from '@egodigital/express-controllers';
/**
* /controllers/index.ts
*
* Base path: '/'
*/
export class Controller extends ControllerBase {
// handle exceptions
public async __error(context: RequestErrorHandlerContext) {
return context.response
.status(500)
.send('SERVER ERROR: ' + context.error);
}
/**
* [GET] / endpoint
*/
@GET()
public async index(req: express.Request, res: express.Response) {
// all request error, like that
// will be handled by
// '__error()' method
throw new Error('Test error!');
}
}
import * as express from 'express';
import { Authorize, AuthorizeFailedHandlerContext, AuthorizeHandlerContext, ControllerBase, GET, RequestErrorHandlerContext } from '@egodigital/express-controllers';
/**
* /controllers/index.ts
*
* Base path: '/'
*/
export class Controller extends ControllerBase {
// check if authorized
public async __authorize(context: AuthorizeHandlerContext) {
// return (true) or (false)
// or a non empty string, which is returned as error message by default
// default: (false)
return 'The authorization has been failed';
}
// handle failed authorization
public async __authorizeFailed(context: AuthorizeFailedHandlerContext) {
// s. result of __authorize()
const ERROR_MSG = context.result as string;
return context.response
.status(401)
.send('AUTHORIZE FAILED: ' + ERROR_MSG);
}
/**
* [GET] / endpoint
*/
@Authorize()
@GET()
public async index(req: express.Request, res: express.Response) {
// this will only be invoked
// if __authorize() returns (true)
// or nothing (null, undefined or empty string)
return res.status(204)
.send();
}
}
First, define the main information of the document:
import * as express from 'express';
import { initControllers } from '@egodigital/express-controllers';
const app = express();
initControllers({
app,
cwd: __dirname + '/controllers',
swagger: {
// s. https://swagger.io/docs/specification/2-0/describing-responses/
definitions: {
'SuccessResponse': {
"type": "object",
"properties": {
"success": {
"description": "Indicates if operation was successful or not.",
"type": "boolean"
},
"data": {
"description": "The result data.",
"type": "string"
},
}
}
},
document: {
host: 'api.example.com',
info: {
contact: {
email: "[email protected]",
},
description: "Describes all API endpoints.",
title: "Test API",
version: "1.0.0",
},
schemes: ['http', 'https'],
tags: {
'test': 'A test tag',
},
},
title: 'Swagger Test',
},
});
app.listen(8080, () => {
console.log('Swagger document: http://localhost:8080/swagger');
});
Now use @Swagger
decorator for each of your method, to document your API (for more information, visit Paths and Operations
):
import { Request, Response } from 'express';
import { ControllerBase, GET, Swagger, SwaggerPathDefinitionUpdaterContext } from '@egodigital/express-controllers';
// update each path definition with default values (s. below)
function pathDefinitionUpdater(ctx: SwaggerPathDefinitionUpdaterContext) {
// Bad Request
ctx.definition['responses']['400'] = {
"description": "Bad request!"
};
// Internal Server Error
ctx.definition['responses']['500'] = {
"description": "Operation has failed!"
};
}
/**
* /controllers/api/index.ts
*
* Base path: '/api'
*/
export class Controller extends ControllerBase {
/**
* [GET] /api
*/
@GET()
// s. https://swagger.io/docs/specification/2-0/paths-and-operations/
@Swagger({
"tags": [
"test"
],
"summary": "A test.",
"produces": [
"application/json"
],
"parameters": [
{
"in": "header",
"name": "X-My-Header",
"required": false,
"type": "string"
},
],
"responses": {
"200": {
"description": "Operation was successful.",
"schema": {
"$ref": "#/definitions/SuccessResponse",
}
},
}
}, pathDefinitionUpdater)
public async index(req: Request, res: Response) {
return res.json({
success: true,
data: 'Swagger test: OK',
});
}
}
The API documentation can be found here.