About us Guides Projects Contacts
Админка
please wait

Express.js is the de facto standard for building Node.js web applications and APIs. Its minimalist, unopinionated design lets you structure applications however you need while providing essential HTTP utilities. This guide covers building production-ready REST APIs with Express from a senior developer's perspective.

Why Express

Express offers compelling advantages:

  1. Minimal Overhead: Thin layer over Node.js HTTP
  2. Flexible: No prescribed structure or patterns
  3. Middleware Ecosystem: Thousands of plugins available
  4. Performance: Fast and battle-tested at scale
  5. Easy to Learn: Simple API, quick to get productive

Getting Started

Installation

mkdir my-api && cd my-api
npm init -y
npm install express
npm install -D nodemon # Auto-restart during development

Basic Server

index.js:

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Welcome to the API' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

Update package.json:

{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
}

Project Structure

Organize for maintainability:

src/
├── config/
│ └── database.js
├── controllers/
│ └── userController.js
├── middleware/
│ ├── auth.js
│ ├── errorHandler.js
│ └── validate.js
├── models/
│ └── User.js
├── routes/
│ ├── index.js
│ └── users.js
├── services/
│ └── userService.js
├── utils/
│ └── helpers.js
└── app.js

Routing

Route Definition

// routes/users.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { authenticate } = require('../middleware/auth');
const { validateUser } = require('../middleware/validate');
// Public routes
router.post('/register', validateUser, userController.register);
router.post('/login', userController.login);
// Protected routes
router.use(authenticate); // All routes below require auth
router.get('/', userController.getAll);
router.get('/:id', userController.getById);
router.put('/:id', validateUser, userController.update);
router.delete('/:id', userController.delete);
module.exports = router;

Mount Routes

// routes/index.js
const express = require('express');
const router = express.Router();
const userRoutes = require('./users');
const postRoutes = require('./posts');
router.use('/users', userRoutes);
router.use('/posts', postRoutes);
// Health check
router.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
module.exports = router;
// app.js
const routes = require('./routes');
app.use('/api/v1', routes);

Controllers

// controllers/userController.js
const userService = require('../services/userService');
exports.getAll = async (req, res, next) => {
try {
const { page = 1, limit = 10, search } = req.query;
const result = await userService.findAll({
page: parseInt(page),
limit: parseInt(limit),
search
});
res.json({
data: result.users,
meta: {
total: result.total,
page: result.page,
limit: result.limit,
pages: Math.ceil(result.total / result.limit)
}
});
} catch (error) {
next(error);
}
};
exports.getById = async (req, res, next) => {
try {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json({ data: user });
} catch (error) {
next(error);
}
};
exports.create = async (req, res, next) => {
try {
const user = await userService.create(req.body);
res.status(201).json({
data: user,
message: 'User created successfully'
});
} catch (error) {
next(error);
}
};
exports.update = async (req, res, next) => {
try {
const user = await userService.update(req.params.id, req.body);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
res.json({
data: user,
message: 'User updated successfully'
});
} catch (error) {
next(error);
}
};
exports.delete = async (req, res, next) => {
try {
const deleted = await userService.delete(req.params.id);
if (!deleted) {
return res.status(404).json({
error: 'User not found'
});
}
res.status(204).send();
} catch (error) {
next(error);
}
};

Middleware

Authentication

// middleware/auth.js
const jwt = require('jsonwebtoken');
exports.authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'No token provided'
});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid token'
});
}
};
exports.authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
};

Validation

// middleware/validate.js
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
role: Joi.string().valid('user', 'admin').default('user')
});
exports.validateUser = (req, res, next) => {
const { error, value } = userSchema.validate(req.body, {
abortEarly: false, // Return all errors
stripUnknown: true // Remove unknown fields
});
if (error) {
const errors = error.details.map(d => ({
field: d.path.join('.'),
message: d.message
}));
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
req.body = value; // Use validated/sanitized values
next();
};

Error Handler

// middleware/errorHandler.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
const errorHandler = (err, req, res, next) => {
console.error('Error:', err);
// Operational errors (expected)
if (err.isOperational) {
return res.status(err.statusCode).json({
error: err.message
});
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
// Mongoose duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(409).json({
error: `${field} already exists`
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: 'Invalid token'
});
}
// Unknown errors (don't leak details in production)
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
};
module.exports = { AppError, errorHandler };

Request Logging

// middleware/logger.js
const morgan = require('morgan');
// Custom token for response time
morgan.token('response-time-ms', (req, res) => {
return `${res.responseTime}ms`;
});
// Development: Colorful, verbose
const devFormat = ':method :url :status :response-time-ms - :res[content-length]';
// Production: JSON for log aggregation
const prodFormat = JSON.stringify({
method: ':method',
url: ':url',
status: ':status',
responseTime: ':response-time',
contentLength: ':res[content-length]',
userAgent: ':user-agent'
});
module.exports = process.env.NODE_ENV === 'production'
? morgan(prodFormat)
: morgan(devFormat);

Complete App Setup

// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const compression = require('compression');
const routes = require('./routes');
const logger = require('./middleware/logger');
const { errorHandler } = require('./middleware/errorHandler');
const app = express();
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: { error: 'Too many requests, please try again later' }
});
app.use('/api', limiter);
// Parsing and compression
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));
app.use(compression());
// Logging
app.use(logger);
// Static files
app.use('/static', express.static('public', {
maxAge: '1d',
etag: true
}));
// Cache control headers
app.get('/static/*', (req, res, next) => {
res.set('Cache-Control', 'public, max-age=3600, must-revalidate');
next();
});
// Routes
app.use('/api/v1', routes);
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error handler (must be last)
app.use(errorHandler);
module.exports = app;

Database Integration

MongoDB with Mongoose

// config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
module.exports = connectDB;
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true, select: false },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
}, { timestamps: true });
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);

Testing

// tests/users.test.js
const request = require('supertest');
const app = require('../src/app');
const mongoose = require('mongoose');
const User = require('../src/models/User');
describe('User API', () => {
beforeAll(async () => {
await mongoose.connect(process.env.TEST_MONGODB_URI);
});
afterAll(async () => {
await mongoose.connection.close();
});
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/v1/users/register', () => {
it('should create a new user', async () => {
const res = await request(app)
.post('/api/v1/users/register')
.send({
name: 'Test User',
email: '[email protected]',
password: 'password123'
});
expect(res.status).toBe(201);
expect(res.body.data).toHaveProperty('id');
expect(res.body.data.email).toBe('[email protected]');
});
it('should return 400 for invalid data', async () => {
const res = await request(app)
.post('/api/v1/users/register')
.send({ email: 'invalid' });
expect(res.status).toBe(400);
expect(res.body).toHaveProperty('error');
});
});
});

Key Takeaways

  1. Separate concerns: Routes, controllers, services, models
  2. Middleware is powerful: Auth, validation, logging, errors
  3. Always handle errors: A global error handler catches everything
  4. Validate input: Never trust client data
  5. Use async/await: Clean async code with try/catch
  6. Security first: Helmet, CORS, rate limiting from day one

Express provides the foundation—your architecture decisions determine maintainability. Keep it simple, test thoroughly, and add complexity only when needed.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive