Создавал ли роутер
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Создание и организация роутеров: от простых примеров к production-ready архитектуре
Да, я создавал роутеры не один раз. Это одна из ключевых частей backend приложения, и правильная организация роутинга влияет на масштабируемость и поддерживаемость всего проекта.
Простой роутер в Express.js
Варианты, с которых я начинал:
// Вариант 1: Все в главном файле (плохо, но было такое)
const express = require('express');
const app = express();
app.get('/users', (req, res) => {
// получить всех пользователей
});
app.post('/users', (req, res) => {
// создать пользователя
});
app.get('/users/:id', (req, res) => {
// получить одного пользователя
});
app.listen(3000);
Проблемы этого подхода:
- Все перемешано в одном файле
- Сложно ориентироваться в коде
- Нельзя переиспользовать логику
- Трудно тестировать
Улучшенный вариант: Разделение на роутеры
// app.js - главный файл
const express = require('express');
const userRouter = require('./routes/users');
const productRouter = require('./routes/products');
const orderRouter = require('./routes/orders');
const app = express();
app.use('/api/v1/users', userRouter);
app.use('/api/v1/products', productRouter);
app.use('/api/v1/orders', orderRouter);
module.exports = app;
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// GET /api/v1/users
});
router.post('/', (req, res) => {
// POST /api/v1/users
});
router.get('/:id', (req, res) => {
// GET /api/v1/users/:id
});
module.exports = router;
Лучше, но есть проблемы:
- Логика всё ещё в роутере
- Нельзя переиспользовать между роутерами
- Нет контроля ошибок
- Нет валидации
Production-ready роутер: с контроллерами и сервисами
Это архитектура, которую я использую в production:
// routes/users.ts (TypeScript для safety)
import express from 'express';
import { UserController } from '../controllers/UserController';
import { authMiddleware } from '../middleware/auth';
import { validateRequest } from '../middleware/validation';
const router = express.Router();
const controller = new UserController();
// Public endpoints
router.post(
'/register',
validateRequest('CreateUserDTO'),
controller.register.bind(controller)
);
router.post('/login', controller.login.bind(controller));
// Protected endpoints
router.use(authMiddleware);
router.get('/', controller.getAll.bind(controller));
router.get('/:id', controller.getById.bind(controller));
router.put('/:id', validateRequest('UpdateUserDTO'), controller.update.bind(controller));
router.delete('/:id', controller.delete.bind(controller));
export default router;
Контроллер:
// controllers/UserController.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/UserService';
import { ApiResponse } from '../types/ApiResponse';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
async getAll(req: Request, res: Response, next: NextFunction) {
try {
const { page = 1, limit = 20 } = req.query;
const users = await this.userService.getAll(
parseInt(page as string),
parseInt(limit as string)
);
res.json({ success: true, data: users });
} catch (error) {
next(error); // Передаем ошибку в error middleware
}
}
async getById(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await this.userService.getById(id);
if (!user) {
return res.status(404).json({ success: false, error: 'User not found' });
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async create(req: Request, res: Response, next: NextFunction) {
try {
const user = await this.userService.create(req.body);
res.status(201).json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async update(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
const user = await this.userService.update(id, req.body);
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async delete(req: Request, res: Response, next: NextFunction) {
try {
const { id } = req.params;
await this.userService.delete(id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}
Сервис:
// services/UserService.ts
import { UserRepository } from '../repositories/UserRepository';
import { CreateUserDTO, UpdateUserDTO } from '../dtos/UserDTO';
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
async getAll(page: number, limit: number) {
return this.userRepository.findAll({ page, limit });
}
async getById(id: string) {
return this.userRepository.findById(id);
}
async create(data: CreateUserDTO) {
// Бизнес-логика
if (await this.userRepository.findByEmail(data.email)) {
throw new Error('User with this email already exists');
}
const hashedPassword = await hashPassword(data.password);
return this.userRepository.create({
...data,
password: hashedPassword
});
}
async update(id: string, data: UpdateUserDTO) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return this.userRepository.update(id, data);
}
async delete(id: string) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
return this.userRepository.delete(id);
}
}
Продвинутые техники организации роутинга
1. Автоматическая регистрация роутов
// app.ts
import path from 'path';
import fs from 'fs';
const registerRoutes = (app: express.Application) => {
const routesDir = path.join(__dirname, 'routes');
const files = fs.readdirSync(routesDir).filter(f => f.endsWith('.ts'));
files.forEach(async (file) => {
const routePath = path.join(routesDir, file);
const module = await import(routePath);
const router = module.default;
const endpoint = `/api/v1/${file.replace('.ts', '')}`;
app.use(endpoint, router);
console.log(`Registered route: ${endpoint}`);
});
};
2. Использование Route Decorators (как в NestJS)
// decorators/Route.ts
type HttpMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
function Route(method: HttpMethod, path: string) {
return function(target: any, propertyKey: string) {
if (!target.constructor.routes) {
target.constructor.routes = [];
}
target.constructor.routes.push({ method, path, handler: propertyKey });
};
}
// controllers/UserController.ts
class UserController {
@Route('get', '/')
async getAll() { }
@Route('get', '/:id')
async getById() { }
@Route('post', '/')
async create() { }
}
3. Role-based Access Control в роутере
const router = express.Router();
// Только для администраторов
router.get(
'/admin/stats',
authMiddleware,
requireRole('admin'),
controller.getAdminStats
);
// Только для владельца ресурса
router.put(
'/:id',
authMiddleware,
requireOwner,
controller.update
);
const requireRole = (role: string) => {
return (req: Request, res: Response, next: NextFunction) => {
if (req.user?.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
Структура папок для роутеров
src/
├── routes/
│ ├── index.ts
│ ├── users.ts
│ ├── products.ts
│ └── orders.ts
├── controllers/
│ ├── UserController.ts
│ ├── ProductController.ts
│ └── OrderController.ts
├── services/
│ ├── UserService.ts
│ ├── ProductService.ts
│ └── OrderService.ts
├── repositories/
│ ├── UserRepository.ts
│ ├── ProductRepository.ts
│ └── OrderRepository.ts
├── middleware/
│ ├── auth.ts
│ ├── validation.ts
│ └── errorHandler.ts
└── app.ts
Обработка ошибок в роутах
// middleware/errorHandler.ts
const errorHandler = (
error: Error,
req: Request,
res: Response,
next: NextFunction
) => {
logger.error('Request error', { error, path: req.path });
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message });
}
if (error instanceof NotFoundError) {
return res.status(404).json({ error: error.message });
}
if (error instanceof UnauthorizedError) {
return res.status(401).json({ error: error.message });
}
// Default error
res.status(500).json({ error: 'Internal server error' });
};
app.use(errorHandler);
Тестирование роутеров
// __tests__/routes/users.test.ts
import request from 'supertest';
import app from '../../app';
describe('User Routes', () => {
test('GET /api/v1/users - should return list of users', async () => {
const response = await request(app)
.get('/api/v1/users')
.expect(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
test('POST /api/v1/users - should create new user', async () => {
const response = await request(app)
.post('/api/v1/users')
.send({
email: 'test@example.com',
password: 'SecurePass123!'
})
.expect(201);
expect(response.body.data.email).toBe('test@example.com');
});
test('GET /api/v1/users/:id - should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/v1/users/non-existent-id')
.expect(404);
expect(response.body.error).toBeDefined();
});
});
Лучшие практики для роутеров
-
Разделение ответственности — роутер только маршрутизирует, контроллер обрабатывает запрос, сервис содержит бизнес-логику
-
Валидация — всегда валидируй входные данные на уровне роутера
-
Консистентные ответы — используй единый формат для всех ответов
-
Обработка ошибок — используй centralised error handling
-
Типизация — используй TypeScript для типов роутов и параметров
-
Документирование — используй OpenAPI/Swagger для документации
-
Тестирование — пиши тесты для всех критичных роутов
Правильно организованные роутеры — это основа масштабируемого и поддерживаемого backend приложения.