Робота з заголовками запиту та відповіді
Робота з заголовками запиту та відповіді
🎯 Мета лекції
- Опанувати використання декоратора @Headers для витягування заголовків HTTP-запиту
- Навчитися встановлювати заголовки HTTP-відповіді через декоратор @Header
- Зрозуміти призначення стандартних HTTP-заголовків у RESTful API
- Вивчити роботу з автентифікаційними заголовками (Authorization, Bearer tokens)
- Практикувати створення кастомних заголовків для метаданих API
- Засвоїти принципи безпечної роботи з заголовками та уникнення витоків даних
🔑 Ключові терміни
- HTTP Header (HTTP-заголовок): пара ключ-значення, що передає метадані про запит або відповідь
- Request Header (заголовок запиту): заголовок, який клієнт надсилає серверу з додатковою інформацією
- Response Header (заголовок відповіді): заголовок, який сервер повертає клієнту для контролю поведінки
- Bearer Token (Bearer-токен): схема автентифікації через заголовок Authorization
- Content Negotiation (узгодження вмісту): процес визначення формату відповіді через заголовки Accept
- CORS (Cross-Origin Resource Sharing): механізм безпеки браузера для контролю міжсайтових запитів
HTTP-заголовки: метадані протоколу
HTTP-заголовки (headers) є критично важливою частиною протоколу HTTP, що дозволяє клієнту та серверу обмінюватися метаданими про запит та відповідь. На відміну від тіла запиту (request body), яке містить основні дані операції, заголовки передають додаткову контекстну інформацію: тип вмісту, мову, автентифікаційні дані, інструкції кешування, відомості про клієнта тощо.
Розуміння HTTP-заголовків є фундаментальним для розробки професійних API. Правильне використання заголовків дозволяє реалізувати автентифікацію, керувати кешуванням, вести логування запитів, впроваджувати версіонування API та забезпечувати безпеку застосунку. Водночас неправильна робота з заголовками може призвести до витоків чутливої інформації, проблем із продуктивністю та вразливостей безпеки.
Анатомія HTTP-повідомлення з заголовками
HTTP-заголовки діляться на кілька категорій за призначенням:
Загальні заголовки (General Headers): застосовуються як до запитів, так і до відповідей
Date— дата та час створення повідомленняConnection— контроль з'єднання (keep-alive, close)Cache-Control— інструкції кешування
Заголовки запиту (Request Headers): надсилаються клієнтом
Host— доменне ім'я сервера (обов'язковий у HTTP/1.1)User-Agent— інформація про клієнтський застосунокAccept— підтримувані формати відповідіAuthorization— автентифікаційні даніReferer— URL сторінки, з якої надійшов запит
Заголовки відповіді (Response Headers): надсилаються сервером
Server— інформація про серверне програмне забезпеченняSet-Cookie— встановлення cookies у клієнтаLocation— URL для перенаправленняWWW-Authenticate— схема автентифікації для 401 Unauthorized
Заголовки представлення (Representation Headers): описують тіло повідомлення
Content-Type— MIME-тип вмісту (application/json, text/html)Content-Length— розмір тіла у байтахContent-Encoding— алгоритм стиснення (gzip, deflate)Content-Language— мова вмісту (uk, en)
Чому заголовки важливі для API
Розглянемо типовий сценарій взаємодії клієнта з API:
1. Клієнт надсилає запит з заголовком Authorization для автентифікації
2. Заголовок Accept: application/json вказує бажаний формат відповіді
3. Заголовок X-Request-ID дозволяє відстежити запит у логах
4. Сервер перевіряє токен з Authorization
5. Сервер встановлює Cache-Control для контролю кешування
6. Сервер додає X-RateLimit-* заголовки для інформування про ліміти
7. Сервер повертає відповідь з Content-Type: application/json
Без правильної роботи з заголовками неможливо реалізувати:
- Автентифікацію та авторизацію — токени передаються через Authorization
- Узгодження вмісту (Content Negotiation) — клієнт вказує бажаний формат через Accept
- Керування кешуванням — Cache-Control та ETag оптимізують продуктивність
- Rate Limiting — інформування клієнта про ліміти запитів
- Версіонування API — X-API-Version дозволяє підтримувати сумісність
- Трейсинг та моніторинг — X-Request-ID зв'язує запити у розподіленій системі
- CORS — Access-Control-* заголовки забезпечують міжсайтову безпеку
Декоратор @Headers: витягування заголовків запиту
NestJS надає декоратор @Headers() для доступу до HTTP-заголовків, що надсилаються клієнтом у запиті. Подібно до декораторів @Param(), @Query() та @Body(), @Headers() має два режими роботи: витягування всіх заголовків як об'єкта або окремого заголовка за ключем.
Режим 1: Витягування всіх заголовків (@Headers())
Коли декоратор використовується без аргументів, він повертає об'єкт з усіма заголовками запиту:
import { Controller, Get, Headers } from '@nestjs/common';
@Controller('debug')
export class DebugController {
@Get('headers')
inspectHeaders(@Headers() headers: Record<string, string>) {
console.log(headers);
// {
// 'host': 'localhost:3000',
// 'user-agent': 'Mozilla/5.0...',
// 'accept': 'application/json',
// 'authorization': 'Bearer eyJhbGc...',
// 'content-type': 'application/json',
// 'x-request-id': 'abc-123-def'
// }
return {
message: 'Request headers received',
headers,
};
}
}
Authorization, AUTHORIZATION та authorization у запиті будуть доступні як headers['authorization'].Режим 2: Витягування окремого заголовка (@Headers('key'))
Частіше використовується підхід з витягуванням конкретних заголовків за ключем:
import { Controller, Get, Headers } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get('profile')
getProfile(
@Headers('authorization') authHeader: string,
@Headers('user-agent') userAgent: string,
) {
console.log('Authorization:', authHeader);
// → "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
console.log('User-Agent:', userAgent);
// → "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."
return {
message: 'Profile data',
client: userAgent,
};
}
}
Якщо заголовок не було передано у запиті, значення буде undefined:
@Get('optional-header')
checkHeader(@Headers('x-custom-header') customHeader?: string) {
if (!customHeader) {
return { message: 'Custom header not provided' };
}
return { message: 'Custom header received', value: customHeader };
}
header?: string) або додавайте валідацію через pipes, щоб обробляти випадки, коли заголовок відсутній:@Get('secure')
secureEndpoint(
@Headers('authorization') authHeader?: string,
) {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Missing or invalid Authorization header');
}
const token = authHeader.substring(7); // Видалити "Bearer "
// Валідація токена...
}
Стандартні HTTP-заголовки запиту
Розглянемо найпоширеніші заголовки, з якими працюють серверні застосунки, та їх практичне застосування у NestJS контролерах.
Authorization: автентифікація користувача
Заголовок Authorization використовується для передачі автентифікаційних даних від клієнта до сервера. Найпопулярніші схеми автентифікації:
Bearer Token (JWT, OAuth 2.0):
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Basic Authentication (застаріле, небезпечне):
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
Приклад витягування токена:
import { Controller, Get, Headers, UnauthorizedException } from '@nestjs/common';
@Controller('api')
export class ApiController {
@Get('protected')
protectedRoute(@Headers('authorization') authHeader?: string) {
if (!authHeader) {
throw new UnauthorizedException('Authorization header is required');
}
// Bearer токен: "Bearer <token>"
const [scheme, token] = authHeader.split(' ');
if (scheme !== 'Bearer' || !token) {
throw new UnauthorizedException('Invalid Authorization format. Expected: Bearer <token>');
}
// Валідація токена (делегується сервісу)
const user = this.authService.validateToken(token);
return {
message: 'Access granted',
user,
};
}
}
Authorization виконується автоматично через Guards (захисники), які ми розглянемо у наступних лекціях. Приклад вище демонструє низькорівневу роботу для розуміння механізму.Content-Type: формат тіла запиту
Заголовок Content-Type вказує MIME-тип даних у тілі запиту. Для API найпоширеніші значення:
Content-Type: application/json # JSON-дані
Content-Type: application/x-www-form-urlencoded # Форма HTML
Content-Type: multipart/form-data # Завантаження файлів
Content-Type: text/plain # Простий текст
Content-Type: application/xml # XML-дані
NestJS автоматично парсить тіло запиту на основі Content-Type, тому зазвичай не потрібно витягувати цей заголовок вручну. Проте іноді потрібна перевірка:
import { Controller, Post, Headers, Body, BadRequestException } from '@nestjs/common';
@Controller('webhooks')
export class WebhooksController {
@Post('github')
handleGitHubWebhook(
@Headers('content-type') contentType: string,
@Body() payload: any,
) {
// GitHub надсилає вебхуки як application/json
if (!contentType.includes('application/json')) {
throw new BadRequestException('Content-Type must be application/json');
}
return this.webhooksService.processGitHubEvent(payload);
}
}
Accept: узгодження формату відповіді
Заголовок Accept дозволяє клієнту вказати, які формати відповіді він підтримує. Сервер може використовувати цю інформацію для узгодження вмісту (content negotiation):
Accept: application/json # Тільки JSON
Accept: application/json, application/xml # JSON або XML
Accept: */* # Будь-який формат
Accept: application/json;q=0.9, text/plain;q=0.5 # З пріоритетами
Приклад узгодження формату:
import { Controller, Get, Headers, NotAcceptableException } from '@nestjs/common';
@Controller('data')
export class DataController {
@Get('export')
exportData(@Headers('accept') accept: string = '*/*') {
const data = { name: 'Example', value: 123 };
// Клієнт хоче JSON
if (accept.includes('application/json')) {
return data; // NestJS автоматично серіалізує у JSON
}
// Клієнт хоче XML
if (accept.includes('application/xml')) {
return `<data><name>${data.name}</name><value>${data.value}</value></data>`;
}
// Клієнт хоче CSV
if (accept.includes('text/csv')) {
return `name,value\n${data.name},${data.value}`;
}
// Непідтримуваний формат
throw new NotAcceptableException('Supported formats: JSON, XML, CSV');
}
}
negotiator:npm install negotiator @types/negotiator
import * as Negotiator from 'negotiator';
@Get('flexible')
flexibleEndpoint(@Headers() headers: Record<string, string>) {
const negotiator = new Negotiator({ headers });
const mediaType = negotiator.mediaType(['application/json', 'application/xml', 'text/csv']);
// Повернути дані у обраному форматі
}
User-Agent: інформація про клієнта
Заголовок User-Agent містить інформацію про клієнтське програмне забезпечення (браузер, мобільний додаток, HTTP-клієнт):
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
User-Agent: PostmanRuntime/7.29.2
User-Agent: MyMobileApp/2.1.0 (iOS 16.0; iPhone14,2)
Практичне застосування:
@Controller('analytics')
export class AnalyticsController {
@Post('track')
trackEvent(
@Headers('user-agent') userAgent: string,
@Body() event: any,
) {
// Логування подій з інформацією про клієнта
this.analyticsService.log({
...event,
client: {
userAgent,
isMobile: this.isMobileDevice(userAgent),
isBot: this.isBot(userAgent),
},
});
return { success: true };
}
private isMobileDevice(userAgent: string): boolean {
return /Mobile|Android|iPhone|iPad/i.test(userAgent);
}
private isBot(userAgent: string): boolean {
return /bot|crawler|spider|crawling/i.test(userAgent);
}
}
Парсинг User-Agent за допомогою бібліотеки ua-parser-js:
npm install ua-parser-js @types/ua-parser-js
import { UAParser } from 'ua-parser-js';
@Get('client-info')
getClientInfo(@Headers('user-agent') userAgent: string) {
const parser = new UAParser(userAgent);
const result = parser.getResult();
return {
browser: result.browser, // { name: 'Chrome', version: '120.0.0.0' }
os: result.os, // { name: 'Mac OS', version: '10.15.7' }
device: result.device, // { type: 'mobile', vendor: 'Apple', model: 'iPhone' }
};
}
Referer: джерело запиту
Заголовок Referer (помилкове написання Referrer) містить URL сторінки, з якої був ініційований запит:
Referer: https://example.com/page
Referer у певних ситуаціях (HTTPS → HTTP, політика Referrer-Policy). Не покладайтеся на цей заголовок для критичної бізнес-логіки або безпеки.Приклад логування джерел трафіку:
@Post('signup')
async createUser(
@Body() createUserDto: CreateUserDto,
@Headers('referer') referer?: string,
) {
const user = await this.usersService.create(createUserDto);
// Логування джерела реєстрації для аналітики
await this.analyticsService.trackSignup({
userId: user.id,
referrer: referer || 'direct',
timestamp: new Date(),
});
return user;
}
Декоратор @Header: встановлення заголовків відповіді
Декоратор @Header() дозволяє встановлювати HTTP-заголовки у відповідях сервера. На відміну від @Headers(), який витягує заголовки з запиту, @Header() встановлює заголовки для відповіді. Цей декоратор застосовується на рівні методу контролера і додає заголовки до всіх відповідей цього методу.
Базовий синтаксис
import { Controller, Get, Header } from '@nestjs/common';
@Controller('data')
export class DataController {
@Get('version')
@Header('X-API-Version', '2.0')
@Header('X-Custom-Header', 'CustomValue')
getVersion() {
return { version: '2.0', status: 'active' };
}
}
Відповідь сервера:
Множинні заголовки відповіді
Декоратор @Header() можна застосовувати кілька разів для встановлення різних заголовків:
@Controller('downloads')
export class DownloadsController {
@Get('report.csv')
@Header('Content-Type', 'text/csv')
@Header('Content-Disposition', 'attachment; filename="report.csv"')
@Header('Cache-Control', 'no-store, no-cache, must-revalidate')
downloadReport() {
const csvData = `Name,Age,Email\nAlice,28,alice@example.com\nBob,32,bob@example.com`;
return csvData;
}
}
У цьому прикладі:
Content-Type: text/csvвказує формат файлуContent-Disposition: attachmentзмушує браузер завантажити файл замість відображенняCache-Controlзабороняє кешування чутливих даних
Content-Disposition: attachment; filename="..." є стандартним способом повідомити браузеру, що відповідь має бути збережена як файл. Параметр filename визначає ім'я файлу за замовчуванням у діалозі збереження.Динамічні заголовки через @Res
Для встановлення заголовків динамічно (на основі логіки виконання) використовуйте об'єкт відповіді Express/Fastify:
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('dynamic')
export class DynamicController {
@Get('headers')
setDynamicHeaders(@Res({ passthrough: true }) res: Response) {
// Встановлення заголовків на основі логіки
const apiVersion = process.env.API_VERSION || '1.0';
res.header('X-API-Version', apiVersion);
const requestId = this.generateRequestId();
res.header('X-Request-ID', requestId);
// Встановлення кількох заголовків одночасно
res.set({
'X-Server-Time': new Date().toISOString(),
'X-Environment': process.env.NODE_ENV,
});
return { message: 'Dynamic headers set', requestId };
}
private generateRequestId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
{ passthrough: true } у декораторі @Res() є критично важливим. Без нього NestJS передає повний контроль над відповіддю Express/Fastify, і ви маєте вручну викликати res.send() або res.json(). З passthrough: true ви можете модифікувати заголовки, але NestJS автоматично відправить дані, повернуті з методу.Практичні сценарії використання заголовків
Cache-Control: керування кешуванням
Заголовок Cache-Control контролює, як клієнти (браузери, проксі-сервери, CDN) мають кешувати відповіді:
@Controller('articles')
export class ArticlesController {
@Get(':id')
@Header('Cache-Control', 'public, max-age=3600') // Кешувати на 1 годину
async getArticle(@Param('id') id: string) {
return this.articlesService.findById(id);
}
@Get(':id/preview')
@Header('Cache-Control', 'private, no-cache, no-store, must-revalidate') // Не кешувати
async getPreview(@Param('id') id: string) {
return this.articlesService.getPreview(id);
}
}
Директиви Cache-Control:
Cache-Control: public, max-age=3600
# public - може кешуватися проксі та CDN
# max-age=3600 - дійсно 3600 секунд (1 година)
Cache-Control: private, max-age=300
# private - тільки браузер, не проксі/CDN
# max-age=300 - дійсно 300 секунд (5 хвилин)
Cache-Control: no-store, no-cache, must-revalidate
# no-store - не зберігати копію взагалі
# no-cache - перевіряти свіжість перед використанням
# must-revalidate - не використовувати застарілий кеш
Cache-Control: public, max-age=0, must-revalidate
# Завжди перевіряти свіжість через ETag/Last-Modified
Динамічне встановлення Cache-Control на основі даних:
@Get(':id')
async getArticle(
@Param('id') id: string,
@Res({ passthrough: true }) res: Response,
) {
const article = await this.articlesService.findById(id);
// Опубліковані статті кешуються, чернетки — ні
if (article.published) {
res.header('Cache-Control', 'public, max-age=3600');
} else {
res.header('Cache-Control', 'private, no-cache');
}
return article;
}
X-RateLimit-*: інформування про ліміти
Заголовки X-RateLimit-* інформують клієнта про залишок доступних запитів та час скидання лічильника:
import { Controller, Get, Headers, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('api')
export class ApiController {
@Get('data')
async getData(
@Headers('x-api-key') apiKey: string,
@Res({ passthrough: true }) res: Response,
) {
// Перевірка лімітів для даного API-ключа
const rateLimitInfo = await this.rateLimitService.checkLimit(apiKey);
// Встановлення заголовків про ліміти
res.set({
'X-RateLimit-Limit': rateLimitInfo.limit.toString(), // Загальний ліміт
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(), // Залишок запитів
'X-RateLimit-Reset': rateLimitInfo.resetTime.toString(), // Unix timestamp скидання
});
// Якщо ліміт вичерпано
if (rateLimitInfo.remaining <= 0) {
res.status(429); // Too Many Requests
return {
message: 'Rate limit exceeded',
retryAfter: rateLimitInfo.resetTime - Date.now(),
};
}
return this.dataService.getData();
}
}
Приклад відповіді:
@nestjs/throttler:npm install @nestjs/throttler
X-RateLimit-* та обробляє логіку лімітування без ручного коду у кожному контролері.X-Request-ID: трейсинг розподілених запитів
Заголовок X-Request-ID (або X-Correlation-ID) дозволяє відстежувати запит через усі сервіси у розподіленій системі:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// Використовуємо ID від клієнта або генеруємо новий
const requestId = req.headers['x-request-id'] as string || uuidv4();
// Зберігаємо у req для використання у контролерах
req['requestId'] = requestId;
// Додаємо у відповідь
res.setHeader('X-Request-ID', requestId);
next();
}
}
Реєстрація middleware у модулі:
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { RequestIdMiddleware } from './middleware/request-id.middleware';
@Module({
// ...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
Використання у контролері:
@Controller('orders')
export class OrdersController {
constructor(
private readonly ordersService: OrdersService,
private readonly logger: Logger,
) {}
@Post()
async createOrder(
@Body() createOrderDto: CreateOrderDto,
@Req() req: Request,
) {
const requestId = req['requestId'];
this.logger.log(`Creating order - Request ID: ${requestId}`, 'OrdersController');
const order = await this.ordersService.create(createOrderDto, requestId);
return order;
}
}
Тепер кожен запит має унікальний ідентифікатор, який передається між сервісами та логується, що дозволяє відстежити весь шлях запиту у розподіленій системі.
Location: перенаправлення на новий ресурс
Заголовок Location використовується зі статусами 201 Created та 3xx Redirect для вказівки URL новоствореного або цільового ресурсу:
import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';
@Controller('users')
export class UsersController {
@Post()
async create(
@Body() createUserDto: CreateUserDto,
@Res({ passthrough: true }) res: Response,
) {
const user = await this.usersService.create(createUserDto);
// Встановлюємо статус 201 Created та заголовок Location
res.status(HttpStatus.CREATED);
res.header('Location', `/users/${user.id}`);
return user;
}
}
Відповідь:
Для перенаправлень використовуйте метод redirect():
@Get('old-path')
redirectToNew(@Res() res: Response) {
// 301 Moved Permanently - постійне перенаправлення
res.redirect(301, '/new-path');
}
@Get('temporary')
temporaryRedirect(@Res() res: Response) {
// 302 Found - тимчасове перенаправлення
res.redirect(302, '/temp-location');
}
Кастомні заголовки: X-префікс та конвенції
Кастомні заголовки (custom headers) дозволяють передавати додаткові метадані, специфічні для вашого API, які не покриваються стандартними HTTP-заголовками. Історично кастомні заголовки використовували префікс X- (наприклад, X-Custom-Header), проте згідно з RFC 6648 цей префікс є застарілим і більше не рекомендується для нових застосунків.
X- (означає "eXperimental" або "eXtension") використовувався для позначення нестандартних заголовків. Проте з часом багато таких заголовків стали фактичними стандартами (X-Forwarded-For, X-Content-Type-Options), що створило плутанину. RFC 6648 (2012) офіційно не рекомендує використовувати X- для нових заголовків, радячи натомість використовувати описові імена без префіксів.Незважаючи на це, багато API продовжують використовувати X- через зворотну сумісність та усталені практики.Загальні рекомендації для кастомних заголовків
Сучасний підхід (без X-):
API-Version: 2.0
Request-ID: abc-123-def
Client-ID: mobile-app-v1.2.3
Correlation-ID: trace-456
Rate-Limit-Remaining: 99
Застарілий, але поширений підхід (з X-):
X-API-Version: 2.0
X-Request-ID: abc-123-def
X-Client-ID: mobile-app-v1.2.3
X-Correlation-ID: trace-456
X-Rate-Limit-Remaining: 99
X- префікс, продовжуйте його використовувати для зворотної сумісності.Приклад: Версіонування API через заголовки
@Controller('api')
export class ApiController {
@Get('data')
@Header('X-API-Version', '2.0')
@Header('X-Deprecated-In', '3.0')
getData(@Headers('x-api-version') requestedVersion?: string) {
// Клієнт може вказати бажану версію у запиті
const version = requestedVersion || '2.0';
if (version === '1.0') {
return this.dataService.getDataV1(); // Застаріла версія
}
return this.dataService.getDataV2(); // Поточна версія
}
@Get('new-data')
@Header('X-API-Version', '3.0')
@Header('X-Introduced-In', '3.0')
getNewData() {
return this.dataService.getDataV3(); // Нова версія з додатковими можливостями
}
}
Приклад: Метадані про процес обробки
@Controller('processing')
export class ProcessingController {
@Post('job')
async createJob(
@Body() jobDto: CreateJobDto,
@Res({ passthrough: true }) res: Response,
) {
const startTime = Date.now();
const job = await this.jobsService.create(jobDto);
const processingTime = Date.now() - startTime;
// Додаємо метадані про обробку
res.set({
'X-Job-ID': job.id,
'X-Processing-Time-Ms': processingTime.toString(),
'X-Queue-Position': job.queuePosition.toString(),
'X-Estimated-Wait-Seconds': job.estimatedWait.toString(),
});
return job;
}
}
Відповідь:
Приклад: Ідентифікація клієнтського застосунку
@Controller('mobile-api')
export class MobileApiController {
@Get('features')
getFeatures(
@Headers('x-client-id') clientId?: string,
@Headers('x-client-version') clientVersion?: string,
@Headers('x-platform') platform?: string,
) {
// Повернути features на основі версії клієнта
const features = this.featuresService.getForClient({
clientId,
version: clientVersion,
platform, // "ios", "android", "web"
});
// Перевірити чи потрібне оновлення
const updateRequired = this.versionService.isUpdateRequired(clientVersion);
return {
features,
updateRequired,
minimumVersion: '1.5.0',
};
}
}
Приклад запиту від мобільного додатку:
curl -H "X-Client-ID: my-mobile-app" \
-H "X-Client-Version: 1.4.2" \
-H "X-Platform: ios" \
http://localhost:3000/mobile-api/features
CORS-заголовки: міжсайтова безпека
CORS (Cross-Origin Resource Sharing) — це механізм безпеки браузера, що контролює, які домени можуть виконувати запити до вашого API. Без правильного налаштування CORS браузери блокуватимуть запити від клієнтів на інших доменах.
Що таке Same-Origin Policy
За замовчуванням браузери дозволяють JavaScript-коду виконувати HTTP-запити лише до того ж походження (origin), з якого завантажено сторінку. Походження складається з трьох компонентів:
https://example.com:443/path
└─┬──┘ └────┬─────┘ └┬┘
схема домен порт
Запити між різними походженнями блокуються:
Приклади перевірки походження:
| Запит від | До | Результат |
|---|---|---|
| https://example.com | https://example.com/api | ✅ Same Origin |
| https://example.com | https://api.example.com | ❌ Інший субдомен |
| https://example.com | http://example.com | ❌ Інша схема |
| https://example.com:443 | https://example.com:8080 | ❌ Інший порт |
| https://example.com | https://example.org | ❌ Інший домен |
Увімкнення CORS у NestJS
NestJS надає вбудовану підтримку CORS через метод enableCors():
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Базове увімкнення CORS для всіх походжень (небезпечно для продакшену!)
app.enableCors();
await app.listen(3000);
}
bootstrap();
Налаштування CORS з обмеженнями
Для продакшену завжди обмежуйте дозволені походження:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: [
'https://frontend.example.com', // Продакшен фронтенд
'http://localhost:3001', // Локальна розробка
],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Total-Count', 'X-Page-Count'], // Доступні у JS
credentials: true, // Дозволити cookies
maxAge: 3600, // Кешування preflight запитів (1 година)
});
await app.listen(3000);
}
Динамічна перевірка походження
Для складніших сценаріїв використовуйте функцію-callback:
app.enableCors({
origin: (origin, callback) => {
// Дозволити запити без origin (наприклад, Postman, мобільні додатки)
if (!origin) {
return callback(null, true);
}
// Дозволені домени
const allowedOrigins = [
'https://frontend.example.com',
'https://admin.example.com',
];
// Дозволити localhost з будь-яким портом у розробці
if (process.env.NODE_ENV === 'development' && origin.includes('localhost')) {
return callback(null, true);
}
// Перевірити чи origin у дозволених
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
});
CORS-заголовки у відповіді
Коли CORS увімкнено, NestJS автоматично додає відповідні заголовки:
origin: '*' з credentials: true! Це є заборонено специфікацією CORS і браузери відхилять такі запити. Якщо потрібні credentials (cookies, Authorization), завжди явно вказуйте дозволені домени.Ручне встановлення CORS-заголовків
У рідкісних випадках може знадобитися ручне встановлення CORS-заголовків для окремих ендпоінтів:
@Controller('public-api')
export class PublicApiController {
@Get('data')
@Header('Access-Control-Allow-Origin', '*')
@Header('Access-Control-Allow-Methods', 'GET, OPTIONS')
@Header('Access-Control-Max-Age', '86400')
getPublicData() {
// Публічний ендпоінт, доступний з будь-якого джерела
return this.dataService.getPublicData();
}
}
Best Practices: безпечна робота з заголовками
Не передавайте чутливі дані у заголовках
Хоча HTTP-заголовки передаються через HTTPS у зашифрованому вигляді, вони часто логуються проксі-серверами, балансувальниками навантаження та системами моніторингу. Ніколи не передавайте чутливі дані у кастомних заголовках:
@Post('transfer')
async transferMoney(
@Headers('x-account-password') password: string, // ❌ Небезпечно!
@Headers('x-credit-card') creditCard: string, // ❌ Небезпечно!
@Body() transferDto: TransferDto,
) {
// ...
}
@Post('transfer')
async transferMoney(
@Headers('authorization') authHeader: string, // ✅ Токен автентифікації
@Body() transferDto: TransferDto, // ✅ Чутливі дані у тілі (зашифровані HTTPS)
) {
// transferDto містить пароль, номер картки тощо
// Тіло запиту не логується проксі-серверами
}
Валідація заголовків через Pipes
Використовуйте pipes для валідації обов'язкових заголовків:
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseAuthHeaderPipe implements PipeTransform {
transform(value: string) {
if (!value) {
throw new BadRequestException('Authorization header is required');
}
const [scheme, token] = value.split(' ');
if (scheme !== 'Bearer' || !token) {
throw new BadRequestException('Invalid Authorization format. Expected: Bearer <token>');
}
return token;
}
}
Використання:
@Get('protected')
getProtectedData(
@Headers('authorization', ParseAuthHeaderPipe) token: string,
) {
// token вже витягнутий та провалідований
return this.dataService.getProtectedData(token);
}
Обмеження розміру заголовків
HTTP-сервери мають ліміт на загальний розмір заголовків (зазвичай 8KB). Не передавайте великі обсяги даних у заголовках:
// ❌ Погано: передача великого JSON у заголовку
@Post('data')
processData(@Headers('x-metadata') metadata: string) {
// metadata може бути кілька кілобайт — це перевантажує заголовки
}
// ✅ Добре: великі дані у тілі запиту
@Post('data')
processData(@Body() data: { metadata: object; payload: any }) {
// metadata у тілі, не обмежена розміром заголовків
}
Документування кастомних заголовків
Завжди документуйте кастомні заголовки через Swagger/OpenAPI:
import { ApiHeader, ApiOperation } from '@nestjs/swagger';
@Controller('api')
export class ApiController {
@Get('data')
@ApiOperation({ summary: 'Get data with optional client identification' })
@ApiHeader({
name: 'X-Client-ID',
description: 'Unique identifier of the client application',
required: false,
example: 'mobile-app-v1.2.3',
})
@ApiHeader({
name: 'X-API-Version',
description: 'Preferred API version',
required: false,
example: '2.0',
})
getData(
@Headers('x-client-id') clientId?: string,
@Headers('x-api-version') apiVersion?: string,
) {
return this.dataService.getData(clientId, apiVersion);
}
}
Уникайте дублювання інформації
Не дублюйте інформацію між заголовками та тілом запиту:
// ❌ Погано: ID користувача і у заголовку, і у тілі
@Post('profile')
updateProfile(
@Headers('x-user-id') userId: string,
@Body() body: { userId: string; name: string },
) {
// Яке userId використовувати, якщо вони різні?
}
// ✅ Добре: ID користувача тільки в одному місці
@Post('profile')
updateProfile(
@Headers('authorization') token: string,
@Body() body: { name: string },
) {
const userId = this.authService.getUserIdFromToken(token);
// Єдине джерело правди
}
Робота з заголовками: практичний приклад
Розглянемо повноцінний приклад контролера, що демонструє різні аспекти роботи з заголовками — автентифікацію, кешування, rate limiting, трейсинг та версіонування:
// articles.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
Headers,
Res,
HttpStatus,
UnauthorizedException,
NotFoundException,
Header,
} from '@nestjs/common';
import { Response } from 'express';
import { ArticlesService } from './articles.service';
import { AuthService } from '../auth/auth.service';
import { RateLimitService } from '../rate-limit/rate-limit.service';
import { CreateArticleDto } from './dto/create-article.dto';
@Controller('articles')
@Header('X-API-Version', '2.0')
@Header('X-Powered-By', 'NestJS')
export class ArticlesController {
constructor(
private readonly articlesService: ArticlesService,
private readonly authService: AuthService,
private readonly rateLimitService: RateLimitService,
) {}
/**
* Отримання списку статей з кешуванням та пагінацією
*/
@Get()
@Header('Cache-Control', 'public, max-age=300') // Кешувати на 5 хвилин
async findAll(
@Headers('x-request-id') requestId: string,
@Headers('accept') accept: string = 'application/json',
@Res({ passthrough: true }) res: Response,
) {
console.log(`[${requestId}] Fetching all articles`);
// Перевірка підтримуваного формату
if (!accept.includes('application/json')) {
res.status(HttpStatus.NOT_ACCEPTABLE);
return { error: 'Only application/json is supported' };
}
const articles = await this.articlesService.findAll();
// Додавання метаданих у заголовки
res.set({
'X-Total-Count': articles.length.toString(),
'X-Page-Size': '20',
'X-Current-Page': '1',
});
return articles;
}
/**
* Отримання окремої статті з умовним кешуванням через ETag
*/
@Get(':id')
async findOne(
@Param('id') id: string,
@Headers('if-none-match') ifNoneMatch: string,
@Headers('x-request-id') requestId: string,
@Res({ passthrough: true }) res: Response,
) {
console.log(`[${requestId}] Fetching article ${id}`);
const article = await this.articlesService.findById(id);
if (!article) {
throw new NotFoundException(`Article with ID ${id} not found`);
}
// Генерація ETag на основі lastModified
const etag = `"${article.lastModified.getTime()}"`;
// Клієнт має актуальну версію?
if (ifNoneMatch === etag) {
res.status(HttpStatus.NOT_MODIFIED);
res.header('ETag', etag);
return; // Не повертаємо тіло, тільки 304 Not Modified
}
// Встановлення заголовків кешування
res.set({
'ETag': etag,
'Cache-Control': 'private, must-revalidate',
'Last-Modified': article.lastModified.toUTCString(),
});
return article;
}
/**
* Створення статті з автентифікацією та rate limiting
*/
@Post()
async create(
@Headers('authorization') authHeader: string,
@Headers('x-request-id') requestId: string,
@Headers('user-agent') userAgent: string,
@Body() createArticleDto: CreateArticleDto,
@Res({ passthrough: true }) res: Response,
) {
console.log(`[${requestId}] Creating article from ${userAgent}`);
// Автентифікація користувача
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException('Authorization header is required');
}
const token = authHeader.substring(7);
const user = await this.authService.validateToken(token);
if (!user) {
throw new UnauthorizedException('Invalid or expired token');
}
// Перевірка rate limits
const rateLimitInfo = await this.rateLimitService.checkLimit(user.id);
res.set({
'X-RateLimit-Limit': rateLimitInfo.limit.toString(),
'X-RateLimit-Remaining': rateLimitInfo.remaining.toString(),
'X-RateLimit-Reset': rateLimitInfo.resetTime.toString(),
});
if (rateLimitInfo.remaining <= 0) {
res.status(HttpStatus.TOO_MANY_REQUESTS);
return {
error: 'Rate limit exceeded',
retryAfter: Math.ceil((rateLimitInfo.resetTime - Date.now()) / 1000),
};
}
// Створення статті
const article = await this.articlesService.create(createArticleDto, user.id);
// Встановлення статусу та Location
res.status(HttpStatus.CREATED);
res.header('Location', `/articles/${article.id}`);
return article;
}
/**
* Експорт статті у різних форматах (Content Negotiation)
*/
@Get(':id/export')
async exportArticle(
@Param('id') id: string,
@Headers('accept') accept: string = 'application/json',
@Res({ passthrough: true }) res: Response,
) {
const article = await this.articlesService.findById(id);
if (!article) {
throw new NotFoundException(`Article with ID ${id} not found`);
}
// JSON (за замовчуванням)
if (accept.includes('application/json')) {
res.header('Content-Type', 'application/json');
return article;
}
// Markdown
if (accept.includes('text/markdown')) {
const markdown = this.articlesService.convertToMarkdown(article);
res.set({
'Content-Type': 'text/markdown; charset=utf-8',
'Content-Disposition': `attachment; filename="article-${id}.md"`,
});
return markdown;
}
// HTML
if (accept.includes('text/html')) {
const html = this.articlesService.convertToHtml(article);
res.header('Content-Type', 'text/html; charset=utf-8');
return html;
}
// PDF
if (accept.includes('application/pdf')) {
const pdf = await this.articlesService.convertToPdf(article);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="article-${id}.pdf"`,
'Content-Length': pdf.length.toString(),
});
return pdf;
}
// Непідтримуваний формат
res.status(HttpStatus.NOT_ACCEPTABLE);
return {
error: 'Unsupported media type',
supportedFormats: [
'application/json',
'text/markdown',
'text/html',
'application/pdf',
],
};
}
}
Цей приклад демонструє:
Читання заголовків запиту:
Authorization— автентифікація через Bearer токенAccept— узгодження формату відповідіIf-None-Match— умовне кешування через ETagX-Request-ID— трейсинг запитівUser-Agent— логування інформації про клієнта
Встановлення заголовків відповіді:
X-API-Version— версіонування API (рівень контролера)Cache-Control— інструкції кешуванняETag/Last-Modified— умовне кешуванняX-RateLimit-*— інформація про лімітиLocation— URL створеного ресурсуContent-Type/Content-Disposition— контроль завантаження файлів
Безпека та Best Practices:
- Валідація формату
Authorization - Перевірка підтримуваних форматів через
Accept - Rate limiting з інформуванням клієнта
- Трейсинг запитів через унікальні ID
- Статуси HTTP: 201 Created, 304 Not Modified, 406 Not Acceptable, 429 Too Many Requests
Перевірка знань
@Headers() — декоратор для витягування (читання) заголовків з HTTP-запиту, що надходить від клієнта. Має два режими:
@Headers()— повертає всі заголовки як об'єкт@Headers('key')— повертає конкретний заголовок
@Header() — декоратор для встановлення (запису) заголовків у HTTP-відповідь, що відправляється клієнту. Застосовується на рівні методу:
@Header('name', 'value')— додає заголовок до всіх відповідей методу
Аналогія: @Headers() читає заголовки ззовні (від клієнта), @Header() записує заголовки назовні (до клієнта).
Згідно зі специфікацією HTTP (RFC 7230), імена заголовків є регістронезалежними — тобто Content-Type, content-type та CONTENT-TYPE є еквівалентними. Проте для спрощення порівняння та пошуку заголовків, Node.js (а також Express та Fastify) нормалізують всі імена заголовків, перетворюючи їх на малі літери при парсингу запиту.
Це означає, що навіть якщо клієнт надіслав заголовок Authorization, у вашому коді він буде доступний як headers['authorization'].
Практичний висновок: Завжди використовуйте малі літери при доступі до заголовків через @Headers('key'):
@Get()
handler(@Headers('authorization') auth: string) {} // ✅ Правильно
handler(@Headers('Authorization') auth: string) {} // ❌ Не спрацює
Так, це стандартна практика — передача токенів автентифікації через заголовок Authorization: Bearer <token> є рекомендованим підходом для RESTful API. Проте важливо дотримуватися наступних правил безпеки:
✅ Безпечні практики:
- Використовувати HTTPS для шифрування всього трафіку (включно з заголовками)
- Застосовувати короткоживучі токени (JWT з терміном дії 15-60 хвилин)
- Реалізовувати refresh tokens для оновлення доступу без повторної автентифікації
- Валідувати токени на сервері через підпис (JWT signature)
❌ Небезпечні практики:
- Передача токенів через URL query-параметри (
?token=...) — логуються у браузерній історії - Відправка токенів через HTTP (без шифрування)
- Зберігання токенів у localStorage без додаткового захисту (XSS-вразливість)
- Використання довгоживучих токенів без можливості відкликання
Альтернативні схеми: Для веб-застосунків можна використовувати httpOnly cookies з SameSite=Strict — це захищає від XSS, проте потребує CSRF-токенів для захисту від CSRF-атак.
Коли ендпоінт вимагає певний заголовок (наприклад, Authorization), а клієнт його не надіслав, сервер має повернути відповідний HTTP-статус помилки з описовим повідомленням:
401 Unauthorized — для відсутнього або некоректного заголовка Authorization:
@Get('protected')
handler(@Headers('authorization') auth?: string) {
if (!auth) {
throw new UnauthorizedException('Authorization header is required');
}
// ...
}
400 Bad Request — для інших обов'язкових заголовків:
@Post('webhook')
handler(@Headers('x-webhook-signature') signature?: string) {
if (!signature) {
throw new BadRequestException('X-Webhook-Signature header is required');
}
// ...
}
Рекомендації:
- Використовуйте ValidationPipe з кастомним DTO для валідації заголовків
- Документуйте обов'язкові заголовки через Swagger (
@ApiHeader({ required: true })) - Повертайте чіткі повідомлення про те, який саме заголовок відсутній та його очікуваний формат
Використовуйте стандартні заголовки, коли це можливо. HTTP-специфікація визначає велику кількість заголовків для різних цілей, і їх використання забезпечує сумісність з інфраструктурою (проксі, балансувальники, кеші).
Створюйте кастомні заголовки для:
- Метаданих, специфічних для вашого бізнесу:
X-Tenant-ID— мультиорендна архітектураX-Feature-Flags— експериментальні функції
- Розширення існуючих механізмів:
X-Request-ID— трейсинг розподілених запитівX-Client-Version— версіонування клієнтського додатку
- Обмежень та метрик:
X-RateLimit-Remaining— залишок запитівX-Processing-Time-Ms— час обробки на сервері
НЕ створюйте кастомні заголовки для:
- Автентифікації — використовуйте стандартний
Authorization - Кешування — використовуйте
Cache-Control,ETag,Last-Modified - Типу вмісту — використовуйте
Content-Type - Узгодження формату — використовуйте
Accept
ETag (Entity Tag) — це унікальний ідентифікатор версії ресурсу, який сервер генерує на основі вмісту (наприклад, хеш даних або timestamp останньої зміни). Механізм умовного кешування працює наступним чином:
Перший запит (кеш порожній):
1. Клієнт → GET /articles/123
2. Сервер генерує ETag: "abc123def" на основі article.lastModified
3. Сервер → 200 OK + ETag: "abc123def" + дані статті
4. Клієнт зберігає дані та ETag у локальному кеші
Наступний запит (перевірка свіжості):
1. Клієнт → GET /articles/123 + If-None-Match: "abc123def"
2. Сервер генерує поточний ETag і порівнює з If-None-Match
3а. Якщо збігається → 304 Not Modified (без тіла, економія трафіку)
3б. Якщо НЕ збігається → 200 OK + новий ETag + оновлені дані
Переваги:
- Економія трафіку — тіло не передається, якщо дані не змінилися
- Гарантія свіжості — клієнт завжди має актуальну версію
- Простота реалізації — не потрібна складна логіка інвалідації кешу
Приклад коду:
@Get(':id')
async getArticle(
@Param('id') id: string,
@Headers('if-none-match') ifNoneMatch: string,
@Res({ passthrough: true }) res: Response,
) {
const article = await this.articlesService.findById(id);
const etag = `"${article.lastModified.getTime()}"`;
if (ifNoneMatch === etag) {
res.status(304); // Not Modified
res.header('ETag', etag);
return; // Без тіла
}
res.header('ETag', etag);
return article; // З тілом
}
Підсумок
У цій лекції ми детально розглянули роботу з HTTP-заголовками у NestJS — від базових декораторів @Headers() та @Header() до складних сценаріїв автентифікації, кешування, CORS та трейсингу розподілених систем.
Ключові висновки:
Декоратори для заголовків:
@Headers()— витягування всіх заголовків запиту@Headers('key')— витягування окремого заголовка запиту@Header('name', 'value')— встановлення заголовків відповіді
Стандартні заголовки:
Authorization— автентифікація через Bearer токениContent-Type/Accept— узгодження формату данихUser-Agent— інформація про клієнтаCache-Control— керування кешуваннямETag/If-None-Match— умовне кешування
Кастомні заголовки:
X-Request-ID— трейсинг розподілених запитівX-API-Version— версіонування APIX-RateLimit-*— інформування про ліміти запитів- Префікс
X-є застарілим, проте широко використовується
CORS:
Access-Control-Allow-Origin— дозволені походженняAccess-Control-Allow-Methods— дозволені HTTP-методиAccess-Control-Allow-Headers— дозволені заголовки- NestJS надає
app.enableCors()для автоматичного налаштування
Best Practices:
- Використовуйте HTTPS для захисту заголовків
- Не передавайте чутливі дані у кастомних заголовках
- Валідуйте обов'язкові заголовки через pipes
- Документуйте кастомні заголовки через Swagger
- Дотримуйтесь стандартів HTTP для сумісності з інфраструктурою
У наступній лекції ми перейдемо до вивчення Data Transfer Objects (DTO) — фундаментального інструменту для типізації, валідації та документування даних, що передаються між клієнтом та сервером.