← Назад к вопросам

Создавал ли роутер

1.0 Junior🔥 121 комментариев
#Node.js и JavaScript#Фреймворки и библиотеки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Создание и организация роутеров: от простых примеров к 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();
  });
});

Лучшие практики для роутеров

  1. Разделение ответственности — роутер только маршрутизирует, контроллер обрабатывает запрос, сервис содержит бизнес-логику

  2. Валидация — всегда валидируй входные данные на уровне роутера

  3. Консистентные ответы — используй единый формат для всех ответов

  4. Обработка ошибок — используй centralised error handling

  5. Типизация — используй TypeScript для типов роутов и параметров

  6. Документирование — используй OpenAPI/Swagger для документации

  7. Тестирование — пиши тесты для всех критичных роутов

Правильно организованные роутеры — это основа масштабируемого и поддерживаемого backend приложения.