Use Case

/

Learning and Development

Implement best practices

Model

Ninja Agent

Produce a Node.js Express boilerplate that follows RESTful best practices—clean architecture, input validation, logging, and unit tests.

Try Query
Ninja Agent
/
coder

Node.js Express RESTful API Boilerplate

Here's a comprehensive Express.js boilerplate that follows RESTful best practices with clean architecture, input validation, logging, and unit tests.

Project Structure

1├── src/
2│   ├── api/
3│   │   ├── controllers/
4│   │   │   ├── user.controller.js
5│   │   │   └── index.js
6│   │   ├── middlewares/
7│   │   │   ├── error.middleware.js
8│   │   │   ├── validation.middleware.js
9│   │   │   └── index.js
10│   │   ├── routes/
11│   │   │   ├── user.routes.js
12│   │   │   └── index.js
13│   │   └── validations/
14│   │       ├── user.validation.js
15│   │       └── index.js
16│   ├── config/
17│   │   ├── environment.js
18│   │   └── logger.js
19│   ├── core/
20│   │   ├── repositories/
21│   │   │   └── user.repository.js
22│   │   └── services/
23│   │       └── user.service.js
24│   ├── models/
25│   │   └── user.model.js
26│   ├── utils/
27│   │   ├── api-error.js
28│   │   └── response.js
29│   └── app.js
30├── tests/
31│   ├── integration/
32│   │   └── user.test.js
33│   ├── unit/
34│   │   ├── controllers/
35│   │   │   └── user.controller.test.js
36│   │   └── services/
37│   │       └── user.service.test.js
38│   └── setup.js
39├── .env.example
40├── .eslintrc.js
41├── .gitignore
42├── jest.config.js
43├── package.json
44└── server.js

Implementation

1. Package Configuration

package.json

1{
2  "name": "express-rest-boilerplate",
3  "version": "1.0.0",
4  "description": "Express RESTful API boilerplate with clean architecture",
5  "main": "server.js",
6  "scripts": {
7    "start": "node server.js",
8    "dev": "nodemon server.js",
9    "lint": "eslint .",
10    "test": "jest",
11    "test:watch": "jest --watch",
12    "test:coverage": "jest --coverage"
13  },
14  "dependencies": {
15    "cors": "^2.8.5",
16    "dotenv": "^16.3.1",
17    "express": "^4.18.2",
18    "express-async-errors": "^3.1.1",
19    "helmet": "^7.0.0",
20    "http-status": "^1.7.0",
21    "joi": "^17.10.1",
22    "morgan": "^1.10.0",
23    "winston": "^3.10.0"
24  },
25  "devDependencies": {
26    "eslint": "^8.49.0",
27    "jest": "^29.6.4",
28    "nodemon": "^3.0.1",
29    "supertest": "^6.3.3"
30  },
31  "engines": {
32    "node": ">=14.0.0"
33  }
34}

2. Server Entry Point

server.js

1require('dotenv').config();
2const app = require('./src/app');
3const logger = require('./src/config/logger');
4const config = require('./src/config/environment');
5
6const server = app.listen(config.port, () => {
7  logger.info(`Server running on port ${config.port}`);
8});
9
10const exitHandler = () => {
11  if (server) {
12    server.close(() => {
13      logger.info('Server closed');
14      process.exit(1);
15    });
16  } else {
17    process.exit(1);
18  }
19};
20
21const unexpectedErrorHandler = (error) => {
22  logger.error(error);
23  exitHandler();
24};
25
26process.on('uncaughtException', unexpectedErrorHandler);
27process.on('unhandledRejection', unexpectedErrorHandler);
28
29process.on('SIGTERM', () => {
30  logger.info('SIGTERM received');
31  if (server) {
32    server.close();
33  }
34});

3. App Configuration

src/app.js

1require('express-async-errors');
2const express = require('express');
3const helmet = require('helmet');
4const cors = require('cors');
5const morgan = require('morgan');
6const routes = require('./api/routes');
7const { errorHandler } = require('./api/middlewares/error.middleware');
8const logger = require('./config/logger');
9
10const app = express();
11
12// Security middleware
13app.use(helmet());
14
15// Parse JSON request body
16app.use(express.json());
17
18// Parse URL-encoded request body
19app.use(express.urlencoded({ extended: true }));
20
21// Enable CORS
22app.use(cors());
23
24// Request logging
25app.use(morgan('combined', { stream: { write: (message) => logger.http(message.trim()) } }));
26
27// API routes
28app.use('/api', routes);
29
30// Health check endpoint
31app.get('/health', (req, res) => {
32  res.status(200).json({ status: 'ok' });
33});
34
35// Error handling middleware
36app.use(errorHandler);
37
38module.exports = app;

4. Environment Configuration

src/config/environment.js

1module.exports = {
2  env: process.env.NODE_ENV || 'development',
3  port: process.env.PORT || 3000,
4  logLevel: process.env.LOG_LEVEL || 'info',
5};

src/config/logger.js

1const winston = require('winston');
2const config = require('./environment');
3
4const enumerateErrorFormat = winston.format((info) => {
5  if (info instanceof Error) {
6    Object.assign(info, { message: info.stack });
7  }
8  return info;
9});
10
11const logger = winston.createLogger({
12  level: config.logLevel,
13  format: winston.format.combine(
14    enumerateErrorFormat(),
15    config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
16    winston.format.splat(),
17    winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
18    winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`)
19  ),
20  transports: [
21    new winston.transports.Console({
22      stderrLevels: ['error'],
23    }),
24  ],
25});
26
27module.exports = logger;

5. Error Handling

src/utils/api-error.js

1class ApiError extends Error {
2  constructor(statusCode, message, isOperational = true, stack = '') {
3    super(message);
4    this.statusCode = statusCode;
5    this.isOperational = isOperational;
6    if (stack) {
7      this.stack = stack;
8    } else {
9      Error.captureStackTrace(this, this.constructor);
10    }
11  }
12}
13
14module.exports = ApiError;

src/api/middlewares/error.middleware.js

1const httpStatus = require('http-status');
2const logger = require('../../config/logger');
3const ApiError = require('../../utils/api-error');
4const config = require('../../config/environment');
5
6const errorHandler = (err, req, res, next) => {
7  let error = err;
8
9  if (!(error instanceof ApiError)) {
10    const statusCode = error.statusCode || httpStatus.INTERNAL_SERVER_ERROR;
11    const message = error.message || httpStatus[statusCode];
12    error = new ApiError(statusCode, message, false, err.stack);
13  }
14
15  const response = {
16    code: error.statusCode,
17    message: error.message,
18    ...(config.env === 'development' && { stack: error.stack }),
19  };
20
21  if (config.env === 'development') {
22    logger.error(error);
23  }
24
25  res.status(error.statusCode).json(response);
26};
27
28module.exports = {
29  errorHandler,
30};

6. Response Utility

src/utils/response.js

1class ApiResponse {
2  constructor(statusCode, data, message = '') {
3    this.statusCode = statusCode;
4    this.data = data;
5    this.message = message;
6    this.success = statusCode >= 200 && statusCode < 300;
7  }
8
9  static success(data, message = '') {
10    return new ApiResponse(200, data, message);
11  }
12
13  static created(data, message = '') {
14    return new ApiResponse(201, data, message);
15  }
16
17  static noContent(message = '') {
18    return new ApiResponse(204, null, message);
19  }
20}
21
22module.exports = ApiResponse;

7. Validation Middleware

src/api/middlewares/validation.middleware.js

1const httpStatus = require('http-status');
2const ApiError = require('../../utils/api-error');
3
4const validate = (schema) => (req, res, next) => {
5  const validSchema = pick(schema, ['params', 'query', 'body']);
6  const object = pick(req, Object.keys(validSchema));
7  const { value, error } = validateSchema(validSchema, object);
8
9  if (error) {
10    const errorMessage = error.details.map((details) => details.message).join(', ');
11    return next(new ApiError(httpStatus.BAD_REQUEST, errorMessage));
12  }
13  
14  Object.assign(req, value);
15  return next();
16};
17
18const validateSchema = (schema, data) => {
19  const options = {
20    abortEarly: false, // include all errors
21    allowUnknown: true, // ignore unknown props
22    stripUnknown: true, // remove unknown props
23  };
24  
25  const { value, error } = schema.validate(data, options);
26  return { value, error };
27};
28
29const pick = (object, keys) => {
30  return keys.reduce((obj, key) => {
31    if (object && Object.prototype.hasOwnProperty.call(object, key)) {
32      obj[key] = object[key];
33    }
34    return obj;
35  }, {});
36};
37
38module.exports = {
39  validate,
40};

8. User Model

src/models/user.model.js

1// This is a simple in-memory model for demonstration
2// In a real app, you would use a database ORM like Sequelize or Mongoose
3
4class User {
5  constructor(id, name, email) {
6    this.id = id;
7    this.name = name;
8    this.email = email;
9    this.createdAt = new Date();
10  }
11}
12
13module.exports = User;

9. User Repository

src/core/repositories/user.repository.js

1const User = require('../../models/user.model');
2
3// In-memory storage for demonstration
4const users = [];
5let nextId = 1;
6
7class UserRepository {
8  async create(userData) {
9    const user = new User(nextId++, userData.name, userData.email);
10    users.push(user);
11    return user;
12  }
13
14  async findById(id) {
15    return users.find(user => user.id === parseInt(id, 10)) || null;
16  }
17
18  async findAll() {
19    return [...users];
20  }
21
22  async update(id, userData) {
23    const index = users.findIndex(user => user.id === parseInt(id, 10));
24    if (index === -1) return null;
25    
26    const updatedUser = { ...users[index], ...userData };
27    users[index] = updatedUser;
28    return updatedUser;
29  }
30
31  async delete(id) {
32    const index = users.findIndex(user => user.id === parseInt(id, 10));
33    if (index === -1) return false;
34    
35    users.splice(index, 1);
36    return true;
37  }
38}
39
40module.exports = new UserRepository();

10. User Service

src/core/services/user.service.js

1const httpStatus = require('http-status');
2const userRepository = require('../repositories/user.repository');
3const ApiError = require('../../utils/api-error');
4
5class UserService {
6  async createUser(userData) {
7    return userRepository.create(userData);
8  }
9
10  async getUserById(id) {
11    const user = await userRepository.findById(id);
12    if (!user) {
13      throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
14    }
15    return user;
16  }
17
18  async getUsers() {
19    return userRepository.findAll();
20  }
21
22  async updateUser(id, userData) {
23    const user = await userRepository.update(id, userData);
24    if (!user) {
25      throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
26    }
27    return user;
28  }
29
30  async deleteUser(id) {
31    const deleted = await userRepository.delete(id);
32    if (!deleted) {
33      throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
34    }
35    return { success: true };
36  }
37}
38
39module.exports = new UserService();

11. User Validation

src/api/validations/user.validation.js

1const Joi = require('joi');
2
3const createUser = {
4  body: Joi.object().keys({
5    name: Joi.string().required(),
6    email: Joi.string().email().required(),
7  }),
8};
9
10const getUser = {
11  params: Joi.object().keys({
12    id: Joi.number().integer().required(),
13  }),
14};
15
16const updateUser = {
17  params: Joi.object().keys({
18    id: Joi.number().integer().required(),
19  }),
20  body: Joi.object().keys({
21    name: Joi.string(),
22    email: Joi.string().email(),
23  }).min(1),
24};
25
26const deleteUser = {
27  params: Joi.object().keys({
28    id: Joi.number().integer().required(),
29  }),
30};
31
32module.exports = {
33  createUser,
34  getUser,
35  updateUser,
36  deleteUser,
37};

12. User Controller

src/api/controllers/user.controller.js

1const httpStatus = require('http-status');
2const userService = require('../../core/services/user.service');
3const ApiResponse = require('../../utils/response');
4
5const createUser = async (req, res) => {
6  const user = await userService.createUser(req.body);
7  res.status(httpStatus.CREATED).json(ApiResponse.created(user, 'User created successfully'));
8};
9
10const getUser = async (req, res) => {
11  const user = await userService.getUserById(req.params.id);
12  res.status(httpStatus.OK).json(ApiResponse.success(user));
13};
14
15const getUsers = async (req, res) => {
16  const users = await userService.getUsers();
17  res.status(httpStatus.OK).json(ApiResponse.success(users));
18};
19
20const updateUser = async (req, res) => {
21  const user = await userService.updateUser(req.params.id, req.body);
22  res.status(httpStatus.OK).json(ApiResponse.success(user, 'User updated successfully'));
23};
24
25const deleteUser = async (req, res) => {
26  await userService.deleteUser(req.params.id);
27  res.status(httpStatus.NO_CONTENT).send();
28};
29
30module.exports = {
31  createUser,
32  getUser,
33  getUsers,
34  updateUser,
35  deleteUser,
36};

13. User Routes

src/api/routes/user.routes.js

1const express = require('express');
2const { validate } = require('../middlewares/validation.middleware');
3const userValidation = require('../validations/user.validation');
4const userController = require('../controllers/user.controller');
5
6const router = express.Router();
7
8router
9  .route('/')
10  .post(validate(userValidation.createUser), userController.createUser)
11  .get(userController.getUsers);
12
13router
14  .route('/:id')
15  .get(validate(userValidation.getUser), userController.getUser)
16  .patch(validate(userValidation.updateUser), userController.updateUser)
17  .delete(validate(userValidation.deleteUser), userController.deleteUser);
18
19module.exports = router;

14. API Routes Index

src/api/routes/index.js

1const express = require('express');
2const userRoutes = require('./user.routes');
3
4const router = express.Router();
5
6const routes = [
7  {
8    path: '/users',
9    route: userRoutes,
10  },
11];
12
13routes.forEach((route) => {
14  router.use(route.path, route.route);
15});
16
17module.exports = router;

15. Unit Tests

tests/unit/services/user.service.test.js

1const httpStatus = require('http-status');
2const userService = require('../../../src/core/services/user.service');
3const userRepository = require('../../../src/core/repositories/user.repository');
4const ApiError = require('../../../src/utils/api-error');
5
6// Mock the repository
7jest.mock('../../../src/core/repositories/user.repository');
8
9describe('User service', () => {
10  describe('createUser', () => {
11    test('should create a user successfully', async () => {
12      const userData = { name: 'Test User', email: 'test@example.com' };
13      const expectedUser = { id: 1, ...userData, createdAt: expect.any(Date) };
14      
15      userRepository.create.mockResolvedValue(expectedUser);
16      
17      const result = await userService.createUser(userData);
18      
19      expect(userRepository.create).toHaveBeenCalledWith(userData);
20      expect(result).toEqual(expectedUser);
21    });
22  });
23
24  describe('getUserById', () => {
25    test('should return user if found', async () => {
26      const userId = 1;
27      const expectedUser = { id: userId, name: 'Test User', email: 'test@example.com' };
28      
29      userRepository.findById.mockResolvedValue(expectedUser);
30      
31      const result = await userService.getUserById(userId);
32      
33      expect(userRepository.findById).toHaveBeenCalledWith(userId);
34      expect(result).toEqual(expectedUser);
35    });
36
37    test('should throw error if user not found', async () => {
38      const userId = 999;
39      
40      userRepository.findById.mockResolvedValue(null);
41      
42      await expect(userService.getUserById(userId)).rejects.toThrow(
43        new ApiError(httpStatus.NOT_FOUND, 'User not found')
44      );
45      
46      expect(userRepository.findById).toHaveBeenCalledWith(userId);
47    });
48  });
49
50  // Additional tests for other methods would follow the same pattern
51});

tests/unit/controllers/user.controller.test.js

1const httpStatus = require('http-status');
2const userController = require('../../../src/api/controllers/user.controller');
3const userService = require('../../../src/core/services/user.service');
4const ApiResponse = require('../../../src/utils/response');
5
6// Mock the service
7jest.mock('../../../src/core/services/user.service');
8
9describe('User controller', () => {
10  let mockReq;
11  let mockRes;
12  let mockNext;
13
14  beforeEach(() => {
15    mockReq = {};
16    mockRes = {
17      status: jest.fn().mockReturnThis(),
18      json: jest.fn(),
19      send: jest.fn(),
20    };
21    mockNext = jest.fn();
22  });
23
24  describe('createUser', () => {
25    test('should create a user and return 201 status', async () => {
26      const userData = { name: 'Test User', email: 'test@example.com' };
27      const createdUser = { id: 1, ...userData };
28      mockReq.body = userData;
29      
30      userService.createUser.mockResolvedValue(createdUser);
31      
32      await userController.createUser(mockReq, mockRes);
33      
34      expect(userService.createUser).toHaveBeenCalledWith(userData);
35      expect(mockRes.status).toHaveBeenCalledWith(httpStatus.CREATED);
36      expect(mockRes.json).toHaveBeenCalledWith(
37        ApiResponse.created(createdUser, 'User created successfully')
38      );
39    });
40  });
41
42  describe('getUser', () => {
43    test('should get a user by id and return 200 status', async () => {
44      const userId = '1';
45      const user = { id: 1, name: 'Test User', email: 'test@example.com' };
46      mockReq.params = { id: userId };
47      
48      userService.getUserById.mockResolvedValue(user);
49      
50      await userController.getUser(mockReq, mockRes);
51      
52      expect(userService.getUserById).toHaveBeenCalledWith(userId);
53      expect(mockRes.status).toHaveBeenCalledWith(httpStatus.OK);
54      expect(mockRes.json).toHaveBeenCalledWith(ApiResponse.success(user));
55    });
56  });
57
58  // Additional tests for other controller methods would follow the same pattern
59});

16. Integration Tests

tests/integration/user.test.js

1const request = require('supertest');
2const httpStatus = require('http-status');
3const app = require('../../src/app');
4
5describe('User API', () => {
6  let newUser;
7  let userId;
8
9  beforeAll(async () => {
10    // Clear any existing data or set up test database
11  });
12
13  describe('POST /api/users', () => {
14    test('should create a new user', async () => {
15      newUser = {
16        name: 'Integration Test User',
17        email: 'integration@test.com',
18      };
19
20      const res = await request(app)
21        .post('/api/users')
22        .send(newUser)
23        .expect(httpStatus.CREATED);
24
25      expect(res.body.data).toHaveProperty('id');
26      expect(res.body.data.name).toBe(newUser.name);
27      expect(res.body.data.email).toBe(newUser.email);
28      
29      userId = res.body.data.id;
30    });
31
32    test('should return 400 error if email is invalid', async () => {
33      const invalidUser = {
34        name: 'Invalid User',
35        email: 'not-an-email',
36      };
37
38      await request(app)
39        .post('/api/users')
40        .send(invalidUser)
41        .expect(httpStatus.BAD_REQUEST);
42    });
43  });
44
45  describe('GET /api/users/:id', () => {
46    test('should get user by id', async () => {
47      const res = await request(app)
48        .get(`/api/users/${userId}`)
49        .expect(httpStatus.OK);
50
51      expect(res.body.data).toHaveProperty('id', userId);
52      expect(res.body.data.name).toBe(newUser.name);
53      expect(res.body.data.email).toBe(newUser.email);
54    });
55
56    test('should return 404 error if user does not exist', async () => {
57      await request(app)
58        .get('/api/users/999')
59        .expect(httpStatus.NOT_FOUND);
60    });
61  });
62
63  // Additional integration tests for other endpoints would follow the same pattern
64});

17. Jest Configuration

jest.config.js

1module.exports = {
2  testEnvironment: 'node',
3  testMatch: ['**/tests/**/*.test.js'],
4  collectCoverageFrom: [
5    'src/**/*.js',
6    '!src/app.js',
7    '!src/config/**/*.js',
8  ],
9  coverageDirectory: 'coverage',
10  setupFilesAfterEnv: ['./tests/setup.js'],
11};

tests/setup.js

1// Setup file for Jest
2// You can add global test setup code here

18. Environment Variables

.env.example

1# Server Configuration
2NODE_ENV=development
3PORT=3000
4LOG_LEVEL=info
5
6# Add other environment variables as needed
7# e.g., DATABASE_URL, JWT_SECRET, etc.

19. ESLint Configuration

.eslintrc.js

1module.exports = {
2  env: {
3    node: true,
4    jest: true,
5  },
6  extends: ['eslint:recommended'],
7  parserOptions: {
8    ecmaVersion: 2020,
9  },
10  rules: {
11    'no-console': 'warn',
12    'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
13  },
14};

20. Git Ignore

.gitignore

1# Dependencies
2node_modules/
3
4# Logs
5logs
6*.log
7npm-debug.log*
8
9# Environment variables
10.env
11.env.local
12.env.development
13.env.test
14.env.production
15
16# Coverage directory
17coverage/
18
19# IDE
20.idea/
21.vscode/
22*.sublime-project
23*.sublime-workspace
24
25# OS
26.DS_Store
27Thumbs.db

Running the Application

  1. Clone the repository
  2. Copy .env.example to .env and adjust as needed
  3. Install dependencies: npm install
  4. Run the server: npm start or npm run dev for development
  5. Run tests: npm test

This boilerplate provides a solid foundation for building RESTful APIs with Node.js and Express, following best practices for clean architecture, validation, error handling, and testing.

Experience Ninja's AI Assistant

Try for free today. Plans starting at $5/month.