Пошук уроків, статей та іншого контенту
Дізнаєтеся, як middleware обробляє HTTP-запити до передавання керування маршруту або наступному middleware.
Middleware — це функція або клас, який отримує HTTP-запит і може виконати певні дії до того, як запит потрапить до обробника маршруту.
У типовому випадку middleware:
отримує об’єкт запиту request;
виконує власну логіку;
викликає next();
передає керування наступному middleware або обробнику маршруту.
Схематично обробка запиту виглядає так:
HTTP-запит
↓
middleware 1
↓
middleware 2
↓
контролер і маршрут
↓
HTTP-відповідьMiddleware часто використовують для:
журналювання запитів;
перевірки заголовків;
додавання даних до об’єкта запиту;
перевірки простих умов перед виконанням маршруту.
Middleware працює на рівні HTTP-запиту. Він не є окремим маршрутом і не замінює контролер.
У NestJS middleware зазвичай є класом, який реалізує інтерфейс NestMiddleware.
Метод use() отримує:
req — вхідний HTTP-запит;
res — HTTP-відповідь;
next — функцію для передачі керування далі.
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(
req: Request,
res: Response,
next: NextFunction,
) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
}У цьому прикладі middleware виводить у консоль HTTP-метод і адресу запиту, а потім викликає next().
Якщо не викликати next() і не сформувати відповідь вручну, запит зупиниться на цьому middleware.
Middleware підключають у модулі, який реалізує інтерфейс NestModule.
Для цього потрібно:
імпортувати MiddlewareConsumer;
реалізувати метод configure();
викликати consumer.apply();
вказати маршрути через forRoutes().
Файл logger.middleware.ts:
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(
req: Request,
res: Response,
next: NextFunction,
) {
console.log(`[${req.method}] ${req.originalUrl}`);
next();
}
}Файл app.controller.ts:
import { Controller, Get } from '@nestjs/common';
@Controller('hello')
export class AppController {
@Get()
getHello() {
return {
message: 'Привіт, NestJS!',
};
}
}Файл app.module.ts:
import {
MiddlewareConsumer,
Module,
NestModule,
} from '@nestjs/common';
import { AppController } from './app.controller';
import { LoggerMiddleware } from './logger.middleware';
@Module({
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(AppController);
}
}Файл main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску застосунку запит:
GET http://localhost:3000/helloспочатку потрапить до LoggerMiddleware. У консолі з’явиться:
[GET] /helloПісля виклику next() запит буде передано методу getHello() контролера.
Метод forRoutes() визначає, для яких маршрутів працюватиме middleware.
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(AppController);
}У такому випадку middleware застосовується до маршрутів контролера AppController.
Для наведеного контролера це маршрут:
GET /helloМожна вказати шлях і HTTP-метод:
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { AppController } from './app.controller';
import { LoggerMiddleware } from './logger.middleware';
@Module({
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({
path: 'hello',
method: RequestMethod.GET,
});
}
}Тепер middleware працюватиме лише для GET /hello.
Щоб застосувати middleware до всіх маршрутів застосунку, можна використати '*':
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}Це означає, що middleware оброблятиме запити до всіх маршрутів, які належать цьому модулю.
До одного маршруту можна підключити кілька middleware:
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware, AnotherMiddleware)
.forRoutes(AppController);
}Вони виконуватимуться в порядку, у якому були передані до apply():
LoggerMiddleware
↓
AnotherMiddleware
↓
обробник маршрутуКожен middleware має передати керування далі, викликавши next().
Middleware не зобов’язаний завжди викликати next(). Він може самостійно завершити відповідь, якщо запит не відповідає певній умові.
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class ApiKeyMiddleware implements NestMiddleware {
use(
req: Request,
res: Response,
next: NextFunction,
) {
const apiKey = req.headers['x-api-key'];
if (apiKey !== 'secret-key') {
res.status(401).json({
message: 'Невірний API-ключ',
});
return;
}
next();
}
}Якщо заголовок x-api-key не містить очікуваного значення, middleware надсилає відповідь зі статусом 401 і завершує виконання.
У цьому випадку next() викликати не потрібно, оскільки запит не повинен потрапити до контролера.
Порівняння потоків:
Коректний API-ключ:
middleware → next() → контролер
Некоректний API-ключ:
middleware → відповідь 401Middleware може додати дані до об’єкта req, щоб наступні етапи обробки могли їх використати.
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(
req: Request,
res: Response,
next: NextFunction,
) {
const requestId = crypto.randomUUID();
req.headers['x-request-id'] = requestId;
next();
}
}Після цього наступний middleware або обробник маршруту може прочитати значення з req.headers['x-request-id'].
На практиці типи об’єкта запиту потрібно враховувати окремо, якщо до нього додаються власні властивості. Для простих прикладів достатньо розуміти сам принцип: middleware може підготувати дані для наступних етапів обробки.
Middleware можна створити не лише класом, а й звичайною функцією:
import { NextFunction, Request, Response } from 'express';
export function loggerMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}Підключення функціонального middleware:
import {
MiddlewareConsumer,
Module,
NestModule,
} from '@nestjs/common';
import { AppController } from './app.controller';
import { loggerMiddleware } from './logger.middleware';
@Module({
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(loggerMiddleware)
.forRoutes(AppController);
}
}Клас middleware зручний, коли потрібно використовувати залежності через механізм dependency injection NestJS. Для невеликої функції функціональний варіант може бути простішим.
Middleware виконується до обробника маршруту.
Наприклад:
@Injectable()
export class FirstMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Перший middleware');
next();
}
}
@Injectable()
export class SecondMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Другий middleware');
next();
}
}Якщо підключити їх у такому порядку:
configure(consumer: MiddlewareConsumer) {
consumer
.apply(FirstMiddleware, SecondMiddleware)
.forRoutes(AppController);
}результат буде таким:
Перший middleware
Другий middleware
метод контролераТому порядок підключення важливий, якщо один middleware залежить від результату роботи іншого.
next()Якщо middleware не надсилає відповідь і не викликає next(), запит зависне:
use(req: Request, res: Response, next: NextFunction) {
console.log('Запит отримано');
// Помилка: керування нікуди не передається
}Потрібно або передати керування:
next();або завершити відповідь:
res.status(403).json({
message: 'Доступ заборонено',
});next() викликається після відправлення відповідіНе слід передавати керування далі після завершення відповіді:
if (!isAllowed) {
res.status(403).json({
message: 'Доступ заборонено',
});
next();
}У цьому випадку контролер може спробувати сформувати ще одну відповідь.
Правильний варіант:
if (!isAllowed) {
res.status(403).json({
message: 'Доступ заборонено',
});
return;
}
next();Якщо middleware не виконується, потрібно перевірити:
чи додано його до apply();
чи правильно вказано forRoutes();
чи збігаються шлях і HTTP-метод;
чи належить контролер потрібному модулю.
Middleware працює із запитом і відповіддю до виконання маршруту. Він не отримує результат методу контролера так, як це роблять деякі інші механізми NestJS.
Middleware обробляє HTTP-запит до передачі керування контролеру.
Клас middleware реалізує NestMiddleware і має метод use().
next() передає запит наступному middleware або обробнику маршруту.
Якщо middleware не викликає next(), він має самостійно завершити відповідь.
Middleware підключають у методі configure() через MiddlewareConsumer.
forRoutes() визначає маршрути, для яких працює middleware.
До одного маршруту можна підключити кілька middleware.
Middleware може журналювати запити, перевіряти умови та додавати дані до об’єкта запиту.