371 lines
13 KiB
Markdown
371 lines
13 KiB
Markdown
# RESTful API Node Server Boilerplate
|
|
|
|
A boilerplate/starter project for quickly building RESTful APIs using [Node.js](https://nodejs.org), [TypeScript](https://www.typescriptlang.org), [Express](https://expressjs.com), and [Prisma](https://www.prisma.io).
|
|
|
|
This project is an adaptation of the project [RESTful API Node Server Boilerplate](https://github.com/hagopj13/node-express-boilerplate) using a [PostgreSQL](https://www.postgresql.org) database with [Prisma](https://www.prisma.io) ORM. Many of the files are just an adaptation to [TypeScript](https://www.typescriptlang.org) from the files of the previously mentioned project.
|
|
|
|
## Quick Start
|
|
|
|
Clone the repo:
|
|
|
|
```bash
|
|
git clone --depth 1 https://github.com/antonio-lazaro/prisma-express-typescript-boilerplate.git
|
|
cd prisma-express-typescript-boilerplate
|
|
npx rimraf ./.git
|
|
```
|
|
|
|
Install the dependencies:
|
|
|
|
```bash
|
|
yarn install
|
|
```
|
|
|
|
Set the environment variables:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
|
|
# open .env and modify the environment variables (if needed)
|
|
```
|
|
|
|
## Table of Contents
|
|
|
|
- [RESTful API Node Server Boilerplate](#restful-api-node-server-boilerplate)
|
|
- [Quick Start](#quick-start)
|
|
- [Table of Contents](#table-of-contents)
|
|
- [Features](#features)
|
|
- [Commands](#commands)
|
|
- [Environment Variables](#environment-variables)
|
|
- [Project Structure](#project-structure)
|
|
- [API Documentation](#api-documentation)
|
|
- [API Endpoints](#api-endpoints)
|
|
- [Error Handling](#error-handling)
|
|
- [Validation](#validation)
|
|
- [Authentication](#authentication)
|
|
- [Authorization](#authorization)
|
|
- [Logging](#logging)
|
|
- [Linting](#linting)
|
|
- [Contributing](#contributing)
|
|
- [Inspirations](#inspirations)
|
|
- [License](#license)
|
|
|
|
## Features
|
|
|
|
- **SQL database**: [PostgreSQL](https://www.postgresql.org) object data modeling using [Prisma](https://www.prisma.io) ORM
|
|
- **Authentication and authorization**: using [passport](http://www.passportjs.org)
|
|
- **Validation**: request data validation using [Joi](https://joi.dev)
|
|
- **Logging**: using [winston](https://github.com/winstonjs/winston) and [morgan](https://github.com/expressjs/morgan)
|
|
- `future` **Testing**: unit and integration tests using [Jest](https://jestjs.io)
|
|
- **Error handling**: centralized error handling mechanism
|
|
- **API documentation**: with [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc) and [swagger-ui-express](https://github.com/scottie1984/swagger-ui-express)
|
|
- **Process management**: advanced production process management using [PM2](https://pm2.keymetrics.io)
|
|
- **Dependency management**: with [Yarn](https://yarnpkg.com)
|
|
- **Environment variables**: using [dotenv](https://github.com/motdotla/dotenv) and [cross-env](https://github.com/kentcdodds/cross-env#readme)
|
|
- **Security**: set security HTTP headers using [helmet](https://helmetjs.github.io)
|
|
- **Santizing**: sanitize request data against xss and query injection
|
|
- **CORS**: Cross-Origin Resource-Sharing enabled using [cors](https://github.com/expressjs/cors)
|
|
- **Compression**: gzip compression with [compression](https://github.com/expressjs/compression)
|
|
- **Docker support**
|
|
- **Code coverage**: using [coveralls](https://coveralls.io)
|
|
- **Code quality**: with [Codacy](https://www.codacy.com)
|
|
- **Git hooks**: with [Husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/okonet/lint-staged)
|
|
- **Linting**: with [ESLint](https://eslint.org) and [Prettier](https://prettier.io)
|
|
- **Editor config**: consistent editor configuration using [EditorConfig](https://editorconfig.org)
|
|
|
|
## Commands
|
|
|
|
Running locally:
|
|
|
|
```bash
|
|
yarn dev
|
|
```
|
|
|
|
Running in production:
|
|
|
|
```bash
|
|
yarn start
|
|
```
|
|
|
|
Testing:
|
|
|
|
```bash
|
|
# run all tests
|
|
yarn test
|
|
|
|
# run all tests in watch mode
|
|
yarn test:watch
|
|
|
|
# run test coverage
|
|
yarn coverage
|
|
```
|
|
|
|
Database:
|
|
|
|
```bash
|
|
# push changes to db
|
|
yarn db:push
|
|
|
|
# start prisma studio
|
|
yarn db:studio
|
|
```
|
|
|
|
Docker:
|
|
|
|
```bash
|
|
# run docker container in development mode
|
|
yarn docker:dev
|
|
|
|
# run docker container in production mode
|
|
yarn docker:prod
|
|
|
|
# run all tests in a docker container
|
|
yarn docker:test
|
|
|
|
# run docker container with PostgreSQL db
|
|
yarn docker:dev-db:start
|
|
|
|
# stop docker container with PostgreSQL db
|
|
yarn docker:dev-db:stop
|
|
```
|
|
|
|
Linting:
|
|
|
|
```bash
|
|
# run ESLint
|
|
yarn lint
|
|
|
|
# fix ESLint errors
|
|
yarn lint:fix
|
|
|
|
# run prettier
|
|
yarn prettier
|
|
|
|
# fix prettier errors
|
|
yarn prettier:fix
|
|
```
|
|
|
|
## Environment Variables
|
|
|
|
The environment variables can be found and modified in the `.env` file. They come with these default values:
|
|
|
|
```bash
|
|
# Port number
|
|
PORT=3000
|
|
|
|
# URL of the PostgreSQL database
|
|
DATABASE_URL=postgresql://postgres:secret@localhost:5432/mydb?schema=public
|
|
|
|
# JWT
|
|
# JWT secret key
|
|
JWT_SECRET=thisisasamplesecret
|
|
# Number of minutes after which an access token expires
|
|
JWT_ACCESS_EXPIRATION_MINUTES=30
|
|
# Number of days after which a refresh token expires
|
|
JWT_REFRESH_EXPIRATION_DAYS=30
|
|
|
|
# SMTP configuration options for the email service
|
|
# For testing, you can use a fake SMTP service like Ethereal: https://ethereal.email/create
|
|
SMTP_HOST=email-server
|
|
SMTP_PORT=587
|
|
SMTP_USERNAME=email-server-username
|
|
SMTP_PASSWORD=email-server-password
|
|
EMAIL_FROM=support@yourapp.com
|
|
```
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
src\
|
|
|--config\ # Environment variables and configuration related things
|
|
|--controllers\ # Route controllers (controller layer)
|
|
|--docs\ # Swagger files
|
|
|--middlewares\ # Custom express middlewares
|
|
|--routes\ # Routes
|
|
|--services\ # Business logic (service layer)
|
|
|--utils\ # Utility classes and functions
|
|
|--validations\ # Request data validation schemas
|
|
|--app.js # Express app
|
|
|--index.js # App entry point
|
|
```
|
|
|
|
## API Documentation
|
|
|
|
To view the list of available APIs and their specifications, run the server and go to `http://localhost:3000/v1/docs` in your browser. This documentation page is automatically generated using the [swagger](https://swagger.io/) definitions written as comments in the route files.
|
|
|
|
### API Endpoints
|
|
|
|
List of available routes:
|
|
|
|
**Auth routes**:\
|
|
`POST /v1/auth/register` - register\
|
|
`POST /v1/auth/login` - login\
|
|
`POST /v1/auth/refresh-tokens` - refresh auth tokens\
|
|
`POST /v1/auth/forgot-password` - send reset password email\
|
|
`POST /v1/auth/reset-password` - reset password\
|
|
`POST /v1/auth/send-verification-email` - send verification email\
|
|
`POST /v1/auth/verify-email` - verify email
|
|
|
|
**User routes**:\
|
|
`POST /v1/users` - create a user\
|
|
`GET /v1/users` - get all users\
|
|
`GET /v1/users/:userId` - get user\
|
|
`PATCH /v1/users/:userId` - update user\
|
|
`DELETE /v1/users/:userId` - delete user
|
|
|
|
## Error Handling
|
|
|
|
The app has a centralized error handling mechanism.
|
|
|
|
Controllers should try to catch the errors and forward them to the error handling middleware (by calling `next(error)`). For convenience, you can also wrap the controller inside the catchAsync utility wrapper, which forwards the error.
|
|
|
|
```javascript
|
|
const catchAsync = require('../utils/catchAsync');
|
|
|
|
const controller = catchAsync(async (req, res) => {
|
|
// this error will be forwarded to the error handling middleware
|
|
throw new Error('Something wrong happened');
|
|
});
|
|
```
|
|
|
|
The error handling middleware sends an error response, which has the following format:
|
|
|
|
```json
|
|
{
|
|
"code": 404,
|
|
"message": "Not found"
|
|
}
|
|
```
|
|
|
|
When running in development mode, the error response also contains the error stack.
|
|
|
|
The app has a utility ApiError class to which you can attach a response code and a message, and then throw it from anywhere (catchAsync will catch it).
|
|
|
|
For example, if you are trying to get a user from the DB who is not found, and you want to send a 404 error, the code should look something like:
|
|
|
|
```javascript
|
|
const httpStatus = require('http-status');
|
|
const ApiError = require('../utils/ApiError');
|
|
const User = require('../models/User');
|
|
|
|
const getUser = async (userId) => {
|
|
const user = await User.findById(userId);
|
|
if (!user) {
|
|
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
|
|
}
|
|
};
|
|
```
|
|
|
|
## Validation
|
|
|
|
Request data is validated using [Joi](https://joi.dev/). Check the [documentation](https://joi.dev/api/) for more details on how to write Joi validation schemas.
|
|
|
|
The validation schemas are defined in the `src/validations` directory and are used in the routes by providing them as parameters to the `validate` middleware.
|
|
|
|
```javascript
|
|
const express = require('express');
|
|
const validate = require('../../middlewares/validate');
|
|
const userValidation = require('../../validations/user.validation');
|
|
const userController = require('../../controllers/user.controller');
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/users', validate(userValidation.createUser), userController.createUser);
|
|
```
|
|
|
|
## Authentication
|
|
|
|
To require authentication for certain routes, you can use the `auth` middleware.
|
|
|
|
```javascript
|
|
const express = require('express');
|
|
const auth = require('../../middlewares/auth');
|
|
const userController = require('../../controllers/user.controller');
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/users', auth(), userController.createUser);
|
|
```
|
|
|
|
These routes require a valid JWT access token in the Authorization request header using the Bearer schema. If the request does not contain a valid access token, an Unauthorized (401) error is thrown.
|
|
|
|
**Generating Access Tokens**:
|
|
|
|
An access token can be generated by making a successful call to the register (`POST /v1/auth/register`) or login (`POST /v1/auth/login`) endpoints. The response of these endpoints also contains refresh tokens (explained below).
|
|
|
|
An access token is valid for 30 minutes. You can modify this expiration time by changing the `JWT_ACCESS_EXPIRATION_MINUTES` environment variable in the .env file.
|
|
|
|
**Refreshing Access Tokens**:
|
|
|
|
After the access token expires, a new access token can be generated, by making a call to the refresh token endpoint (`POST /v1/auth/refresh-tokens`) and sending along a valid refresh token in the request body. This call returns a new access token and a new refresh token.
|
|
|
|
A refresh token is valid for 30 days. You can modify this expiration time by changing the `JWT_REFRESH_EXPIRATION_DAYS` environment variable in the .env file.
|
|
|
|
## Authorization
|
|
|
|
The `auth` middleware can also be used to require certain rights/permissions to access a route.
|
|
|
|
```javascript
|
|
const express = require('express');
|
|
const auth = require('../../middlewares/auth');
|
|
const userController = require('../../controllers/user.controller');
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/users', auth('manageUsers'), userController.createUser);
|
|
```
|
|
|
|
In the example above, an authenticated user can access this route only if that user has the `manageUsers` permission.
|
|
|
|
The permissions are role-based. You can view the permissions/rights of each role in the `src/config/roles.js` file.
|
|
|
|
If the user making the request does not have the required permissions to access this route, a Forbidden (403) error is thrown.
|
|
|
|
## Logging
|
|
|
|
Import the logger from `src/config/logger.js`. It is using the [Winston](https://github.com/winstonjs/winston) logging library.
|
|
|
|
Logging should be done according to the following severity levels (ascending order from most important to least important):
|
|
|
|
```javascript
|
|
const logger = require('<path to src>/config/logger');
|
|
|
|
logger.error('message'); // level 0
|
|
logger.warn('message'); // level 1
|
|
logger.info('message'); // level 2
|
|
logger.http('message'); // level 3
|
|
logger.verbose('message'); // level 4
|
|
logger.debug('message'); // level 5
|
|
```
|
|
|
|
In development mode, log messages of all severity levels will be printed to the console.
|
|
|
|
In production mode, only `info`, `warn`, and `error` logs will be printed to the console.\
|
|
It is up to the server (or process manager) to actually read them from the console and store them in log files.\
|
|
This app uses pm2 in production mode, which is already configured to store the logs in log files.
|
|
|
|
Note: API request information (request url, response code, timestamp, etc.) are also automatically logged (using [morgan](https://github.com/expressjs/morgan)).
|
|
|
|
## Linting
|
|
|
|
Linting is done using [ESLint](https://eslint.org/) and [Prettier](https://prettier.io).
|
|
|
|
In this app, ESLint is configured to follow the [Airbnb JavaScript style guide](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base) with some modifications. It also extends [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) to turn off all rules that are unnecessary or might conflict with Prettier.
|
|
|
|
To modify the ESLint configuration, update the `.eslintrc.json` file. To modify the Prettier configuration, update the `.prettierrc.json` file.
|
|
|
|
To prevent a certain file or directory from being linted, add it to `.eslintignore` and `.prettierignore`.
|
|
|
|
To maintain a consistent coding style across different IDEs, the project contains `.editorconfig`
|
|
|
|
## Contributing
|
|
|
|
Contributions are more than welcome! Please check out the [contributing guide](CONTRIBUTING.md).
|
|
|
|
## Inspirations
|
|
|
|
- [RESTful API Node Server Boilerplate](https://github.com/hagopj13/node-express-boilerplate)
|
|
|
|
## License
|
|
|
|
[MIT](LICENSE)
|