Boilerplate to work with express using typescript. The boilerplate includes:
- Controller decorators to make the job of creating routes easier
- Schema validation middlewares using Joi
- API documentation using Swagger
- Custom and flexible configuration setup with support to docker secrets
- Database models using Sequelize
- Error handling middlewares and utilities on top of http-erros
- View capabilities using Pug
- Docker setup files, including Dockerfile and docker-compose
- ESLint to check code quality
- Setup
- Logging
- Route controllers
- Validation
- Error handling
- API Documentation
- Database models
- Public folder
- ESLint support
To run application first clone the repo using:
> git clone https://github.com/rracanicci/express-tsboilerplate.git
Install dependencies using npm:
> npm install
Build the application using:
> npm run build
As the code uses typescript, this command will cleanup any previoues builds from the dist directory, build the application using tsc and than copy src/img, src/public, src/views and src/api-docs to the dist folder.
To actually bring the application up you can use the following code to start using raw node:
> npm start
Or use the command bellow to start in debug mode using nodemon:
> npm run debug
Be aware that nodemon will listen for changes on the dist folder. As we are using typescript you will have to rebuild the application in order to nodemon to see changes in the transpiled javascript files; Alternativelly, you can run tsc in watch mode along with npm run debug
:
- Terminal 1:
> npx tsc --watch
- Terminal 2:
> npm run debug
To see more, check the scripts session at the package.json file.
To run using docker simple run:
> docker-compose up --build
This will build the image following the Dockerfile and bring the container up. The application will than be available at the default port 3000.
Configuration can be read from the configuration file. You can access the application's configurations with just:
import config from 'config';
For each configuration there is a linked environment variable which can be set to tune the configuration. Besides, if _FILE is added to the variable name, the application will look for the file pointed by the variable value and load the config value from the file (helpful to use with docker secrets).
The current configuration is:
const config = {
// application configuration
app: {
// port the application should run
port: +getVar('PORT', '3000'),
// indicaticates wheather errors should be returned as JSON
// or if a HTML page should be rendered
jsonError: string2Bool(
getVar('JSON_ERROR', 'false')
)
},
// sequelize configuration
db: {
database: getVar('DB_DATABASE', 'db_dev'),
username: getVar('DB_USERNAME', ''),
password: getVar('DB_PASSWORD', ''),
// provides support to all sequelize options
options: {
dialect: getVar('DB_DIALECT', 'sqlite'),
storage: getVar('DB_STORAGE', 'db.sqlite')
}
},
// application environment
nodeenv: (
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'test'
) ? process.env.NODE_ENV : 'development'
};
The getVar function looks for the environment varible, if the it is not found, the default value passed as second parameter will be used. For more info check here.
For logging the application relies on the debug module. All the log message are prefixed with app. You can use the DEBUG environment variable to filter the wanted messages. For instance:
> export DEBUG=app:*
In order to easly create application routes a set of decorators was developed at the controller-base utility file:
- Controller
- Get
- Post
- Delete
- Options
- Put
- All
- Use
To create a set of routes, just create a controller file such as:
@Controller('/api/example')
export class ExampleRouter {
@Get('/')
public async get(
req: Request, res: Response, next: NextFunction
): Promise<void> { ... }
@Post('/')
public async create(
req: Request, res: Response, next: NextFunction
): Promise<void> { ... }
@Put('/:id')
public async update(
req: Request, res: Response, next: NextFunction
): Promise<void> { ... }
@Delete('/:id')
public async delete(
req: Request, res: Response, _next: NextFunction
): Promise<void> { ... }
}
To make the route visible, go to the routes file and import your controller:
/*
import all your controller routes here
*/
import './controllers/index';
import './controllers/swagger';
import './controllers/api/users';
import './controolers/api/example';
Once you do this, the function configureControllers from the controller-base will automatically map and create the routes (check the app.ts file).
If you need to add middlewares for a specific route or even to a entire controller, just add the middlewares to be called before the route in the decorator call:
@Controller('/api/example', <your middlewares go here>)
export class ExampleRouter {
@Get('/', <your middlewares go here>)
public async get(
req: Request, res: Response, next: NextFunction
): Promise<void> { ... }
For a full example, check the example /api/users route.
To validate URL parameters, query parameters and JSON body parameters a set of middlewares is avaible at the validation middleware file:
- validateParams
- validateQuery
- validateBody
Add them as a pre requirement for a route or controller. They will perform the validation using a Joi Schema and return a Bad Request with the correct error message in case validation fails. If validation succeeds, the validated objects (req.params, req.query and req.body) will be replaced in the request, including type convertions and default values:
@Controller('/api/users')
export class UsersRouter {
@Put(
'/:id',
validateParams(
joi.object({
id: joi.number().positive().required()
})
),
validateBody(
joi.object({
name: joi.string().min(1).max(255).required()
})
)
)
public async update(
req: Request, res: Response, next: NextFunction
): Promise<void> {
const { id } = req.params;
const { name } = req.body;
...
}
}
A full example can be seen at the /api/users route.
An error handling middleware is used to handle errors. It can both render a view or return a JSON error depending on the JSON_ERROR variable value.
Also, all routes and middlewares used with the decoratoros described here will be wrapped with the function throwError, that automatically redirects uncatched erros to the error handling middleware.
To provide API documentation the project uses the modules swagger-jsdoc and swagger-ui-express. The configuration for them can be found in the swagger config file.
So, to documment an API you can boh add comments to the code, as the IndexRouter example:
@Controller()
export class IndexRouter {
/**
* @swagger
* /:
* get:
* tags:
* - Index
* summary: Index Page.
* description: Just a simple index page
*/
@Get(
'/',
validateQuery(
joi.object({
title: joi.string().default('express-tsboilerplate').optional()
})
)
)
public getIndex(req: Request, res: Response, _next: NextFunction): void {
const { title } = req.query;
...
Or create a swagger definition file under the folder src/api-docs.
Once you bring the application up there will be a route /swagger with the documentation.
If you need to work with databases, the Sequelize module can be very useful. You can use it to define models and easely manipulate data in the application. The boilerplate has been already configured to work with Sequelize.
To create a new model, go to the models directory and create a file to represent the model, such as user.ts:
import { DataTypes } from 'sequelize';
import { db, CustomModel } from '../sequelize';
export class User extends CustomModel {
public name!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
}
User.init(
{
name: {
type: new DataTypes.STRING,
allowNull: false,
unique: true
}
},
{
sequelize: db, // passing the `sequelize` instance is required
}
);
export function associate(): void {
return;
}
The associate function can be used to define associatons with other modules, such as belongsToMany, belongsTo, hasOne and hasMany. For more information check the official sequelize documentation.
The index file is responsible for importing all the modules, creating the associations and making the modules visible to sequelize. So it should be imported once into your application, such as in the app.ts file:
import './routes';
import './db/models'; // <--
import path from 'path';
import debug from 'debug';
import cookieParser from 'cookie-parser';
import morgan from 'morgan';
import favicon from 'serve-favicon';
import cors from 'cors';
import express from 'express';
import config from './config';
import { NotFound } from 'http-errors';
import { handleError } from './middlewares/error';
import { Request, Response, NextFunction } from 'express';
import { configureControllers } from './utils/controller-base';
import { json2String } from './utils/parsers';
/*
declarations
*/
const logger = debug('app:app');
/*
app setup
*/
const app = express();
If you need to add methods to all models at once, just add them to the base model:
import config from '../config';
import { Sequelize, Model } from 'sequelize';
/*
use this model do add extra features to all models
*/
export class CustomModel extends Model {} // <--
/*
sequelize instance to be used application wide
*/
export const db = new Sequelize(
config.db.database,
config.db.username,
config.db.password,
config.db.options
);
In order to use and/or query models, just import them:
import _ from 'lodash';
import joi from 'joi';
import { Request, Response, NextFunction } from 'express';
import { Controller, Get, Post, Put, Delete } from '../../utils/controller-base';
import { validateQuery, validateBody, validateParams } from '../../middlewares/validation';
import { User } from '../../db/models/user'; // <--
import { Op, UniqueConstraintError } from 'sequelize';
import { Conflict, NotFound } from 'http-errors';
import { db } from '../../db/sequelize';
@Controller('/api/users')
export class UsersRouter {
@Get(
'/',
validateQuery(
joi.object({
id: joi.number().positive().optional(),
name: joi.string().min(1).max(255).optional()
})
)
)
public async get(
req: Request, res: Response, next: NextFunction
): Promise<void> {
const { id, name } = req.query;
const users: User[] = await User.findAll({ // <--
where: _.pickBy({
id: id,
name: name ? {
[Op.like]: `%${name}%`
} : undefined
}, _.identity) as any
});
if (users.length == 0) {
return next(new NotFound('no user found'));
}
res.json(users);
}
...
A full example can be found here.
To destroy the database and rebuild based on the models call:
> npm run syncdb
More details can be found in this file.
If you need to expose some static files, just place them inside the src/public and access it through the route /public/....
This feature is defined at the app.ts file:
// serving static files
app.use('/public', express.static(path.join(__dirname, 'public')));
ESLint configuration can be found in the file .eslintrc.json. To run use:
> npm run eslint