Разработка бэкенда сайта на Node.js (Express)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка бэкенда сайта на Node.js (Express)
Средняя
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка бэкенда сайта на Node.js (Express)

Express остаётся прагматичным выбором для бэкенда веб-сайта: минимум магии, предсказуемое поведение, огромная экосистема middleware. Не самый быстрый фреймворк (Fastify быстрее), не самый feature-rich (NestJS богаче), но сочетание простоты и гибкости делает его рабочим инструментом для большинства задач.

Структура проекта

Плоская структура «все файлы в routes/» работает до определённого масштаба. При росте проекта нужна явная многослойная архитектура:

src/
├── config/
│   ├── env.ts             # typed env validation (zod)
│   └── database.ts
├── modules/
│   ├── users/
│   │   ├── users.router.ts
│   │   ├── users.service.ts
│   │   ├── users.repository.ts
│   │   ├── users.schema.ts    # Zod схемы валидации
│   │   └── users.types.ts
│   ├── products/
│   └── orders/
├── middleware/
│   ├── auth.ts
│   ├── errorHandler.ts
│   ├── requestLogger.ts
│   └── rateLimit.ts
├── lib/
│   ├── database.ts        # Prisma client
│   ├── redis.ts
│   ├── mailer.ts
│   └── queue.ts
└── app.ts

Настройка приложения

// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { pinoHttp } from 'pino-http';
import { usersRouter } from './modules/users/users.router';
import { productsRouter } from './modules/products/products.router';
import { ordersRouter } from './modules/orders/orders.router';
import { errorHandler } from './middleware/errorHandler';
import { notFound } from './middleware/notFound';
import { env } from './config/env';

export function createApp() {
  const app = express();

  // Безопасность
  app.use(helmet());
  app.use(cors({
    origin: env.ALLOWED_ORIGINS.split(','),
    credentials: true,
  }));

  // Парсинг
  app.use(express.json({ limit: '1mb' }));
  app.use(express.urlencoded({ extended: true, limit: '1mb' }));

  // Логирование запросов
  app.use(pinoHttp({
    logger: logger,
    customLogLevel: (_req, res) => {
      if (res.statusCode >= 500) return 'error';
      if (res.statusCode >= 400) return 'warn';
      return 'info';
    },
  }));

  // Healthcheck (без auth и rate limit)
  app.get('/health', (_req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
  });

  // Роуты
  app.use('/api/users', usersRouter);
  app.use('/api/products', productsRouter);
  app.use('/api/orders', ordersRouter);

  // Обработка ошибок — должна быть последней
  app.use(notFound);
  app.use(errorHandler);

  return app;
}
// src/config/env.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  JWT_REFRESH_SECRET: z.string().min(32),
  ALLOWED_ORIGINS: z.string().default('http://localhost:5173'),
});

export const env = envSchema.parse(process.env);

Падение при старте с явным сообщением об ошибке лучше, чем непонятное поведение в рантайме при отсутствии переменной окружения.

Паттерн Router → Service → Repository

// modules/products/products.router.ts
import { Router } from 'express';
import { authenticate } from '../../middleware/auth';
import { validate } from '../../middleware/validate';
import { ProductsService } from './products.service';
import { createProductSchema, updateProductSchema, listProductsSchema } from './products.schema';

const service = new ProductsService();
export const productsRouter = Router();

productsRouter.get(
  '/',
  validate({ query: listProductsSchema }),
  async (req, res, next) => {
    try {
      const result = await service.list(req.query as any);
      res.json(result);
    } catch (err) { next(err); }
  }
);

productsRouter.get('/:id', async (req, res, next) => {
  try {
    const product = await service.getById(req.params.id);
    if (!product) return res.status(404).json({ error: 'Товар не найден' });
    res.json(product);
  } catch (err) { next(err); }
});

productsRouter.post(
  '/',
  authenticate,
  validate({ body: createProductSchema }),
  async (req, res, next) => {
    try {
      const product = await service.create(req.body, req.user!.id);
      res.status(201).json(product);
    } catch (err) { next(err); }
  }
);
// modules/products/products.service.ts
import { ProductsRepository } from './products.repository';
import type { CreateProductDto, ListProductsQuery } from './products.types';
import { redis } from '../../lib/redis';

export class ProductsService {
  private repo = new ProductsRepository();

  async list(query: ListProductsQuery) {
    const cacheKey = `products:list:${JSON.stringify(query)}`;
    const cached = await redis.get(cacheKey);
    if (cached) return JSON.parse(cached);

    const result = await this.repo.findMany(query);
    await redis.set(cacheKey, JSON.stringify(result), 'EX', 300);
    return result;
  }

  async getById(id: string) {
    const cacheKey = `products:${id}`;
    const cached = await redis.get(cacheKey);
    if (cached) return JSON.parse(cached);

    const product = await this.repo.findById(id);
    if (product) await redis.set(cacheKey, JSON.stringify(product), 'EX', 3600);
    return product;
  }

  async create(data: CreateProductDto, createdBy: string) {
    const product = await this.repo.create({ ...data, createdBy });
    // Инвалидация списочного кеша
    const keys = await redis.keys('products:list:*');
    if (keys.length > 0) await redis.del(keys);
    return product;
  }
}

Middleware: валидация, аутентификация, ошибки

// middleware/validate.ts — типизированная валидация через Zod
import type { Request, Response, NextFunction } from 'express';
import type { ZodSchema } from 'zod';

interface ValidateSchemas {
  body?: ZodSchema;
  query?: ZodSchema;
  params?: ZodSchema;
}

export function validate(schemas: ValidateSchemas) {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      if (schemas.body) {
        req.body = schemas.body.parse(req.body);
      }
      if (schemas.query) {
        req.query = schemas.query.parse(req.query) as any;
      }
      if (schemas.params) {
        req.params = schemas.params.parse(req.params) as any;
      }
      next();
    } catch (err) {
      if (err instanceof ZodError) {
        return res.status(422).json({
          error: 'Validation failed',
          details: err.flatten().fieldErrors,
        });
      }
      next(err);
    }
  };
}

// middleware/errorHandler.ts
export function errorHandler(
  err: Error,
  _req: Request,
  res: Response,
  _next: NextFunction
) {
  // Логируем с stack trace только в dev
  logger.error({ err }, 'Unhandled error');

  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
    });
  }

  // Prisma: запись не найдена
  if (err.constructor.name === 'NotFoundError') {
    return res.status(404).json({ error: 'Not found' });
  }

  // Prisma: нарушение уникального ограничения
  if ((err as any).code === 'P2002') {
    return res.status(409).json({ error: 'Already exists' });
  }

  const status = process.env.NODE_ENV === 'production' ? 500 : 500;
  const message = process.env.NODE_ENV === 'production'
    ? 'Internal server error'
    : err.message;

  res.status(status).json({ error: message });
}

Аутентификация через JWT

// middleware/auth.ts
import type { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { env } from '../config/env';

interface JwtPayload {
  sub: string;
  role: string;
  iat: number;
  exp: number;
}

declare global {
  namespace Express {
    interface Request {
      user?: { id: string; role: string };
    }
  }
}

export function authenticate(req: Request, res: Response, next: NextFunction) {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const token = authHeader.slice(7);

  try {
    const payload = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
    req.user = { id: payload.sub, role: payload.role };
    next();
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

export function authorize(...roles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

Graceful shutdown

// src/server.ts
import { createApp } from './app';
import { prisma } from './lib/database';
import { redis } from './lib/redis';
import { env } from './config/env';

const app = createApp();

const server = app.listen(env.PORT, () => {
  logger.info({ port: env.PORT }, 'Server started');
});

async function shutdown(signal: string) {
  logger.info({ signal }, 'Shutdown signal received');

  server.close(async () => {
    await prisma.$disconnect();
    await redis.quit();
    logger.info('Server closed gracefully');
    process.exit(0);
  });

  // Force kill после 10 секунд
  setTimeout(() => {
    logger.error('Forced shutdown');
    process.exit(1);
  }, 10_000);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Тестирование

// modules/products/products.service.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ProductsService } from './products.service';
import { ProductsRepository } from './products.repository';

vi.mock('./products.repository');
vi.mock('../../lib/redis', () => ({
  redis: { get: vi.fn().mockResolvedValue(null), set: vi.fn(), del: vi.fn(), keys: vi.fn().mockResolvedValue([]) }
}));

describe('ProductsService', () => {
  let service: ProductsService;
  let repo: ProductsRepository;

  beforeEach(() => {
    repo = new ProductsRepository();
    service = new ProductsService();
    (service as any).repo = repo;
  });

  it('возвращает товар из кеша при повторном запросе', async () => {
    const product = { id: '1', name: 'Test', price: 100 };
    vi.mocked(repo.findById).mockResolvedValue(product as any);

    await service.getById('1');
    await service.getById('1');

    expect(repo.findById).toHaveBeenCalledTimes(1);
  });
});

Сроки

REST API для сайта среднего размера (10–15 модулей, аутентификация, файлы, уведомления) — четыре-шесть недель. Включает: настройку окружения и CI/CD, разработку модулей, написание тестов, интеграцию с внешними сервисами, документацию API (OpenAPI/Swagger).