Пошук уроків, статей та іншого контенту
Повноцінний REST API з користувачами, JWT-автентифікацією, валідацією, обробкою помилок і Docker-конфігурацією для локального запуску.
Вимоги
Етапи
Мінімальний NestJS-сервер створено через Nest CLI та успішно запускається локально.
Застосунок розбитий на фічеві модулі (users, auth) замість одного величезного AppModule — як в уроці «Модулі: організація застосунку».
PrismaClient доступний як звичайний, injectable провайдер у будь-якому модулі, що його потребує.
На відміну від Next.js (простий модульний синглтон), NestJS обгортає PrismaClient у клас-провайдер, керований власним IoC-контейнером (курс NestJS, урок про Dependency Injection):
src/prisma/prisma.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
}Цей проєкт зводить докупи весь курс NestJS в один робочий API: модулі й контролери для структури, провайдери й Dependency Injection для бізнес-логіки, Pipes для валідації, Guards для автентифікації, Exception Filters для помилок — саме в тому порядку, у якому конвеєр обробки запиту (урок «Життєвий цикл запиту в NestJS») їх насправді викликає.
Ініціалізуйте проєкт через Nest CLI та переконайтесь, що мінімальний сервер запускається.
Продумайте архітектуру: окремий модуль users, окремий модуль auth.
Підключіть Prisma як провайдер, доступний через Dependency Injection.
Реалізуйте CRUD-ендпоінти для користувачів у UsersController/UsersService.
Додайте автентифікацію через JWT і Guard, що перевіряє токен.
Опишіть DTO з правилами валідації та підключіть ValidationPipe.
Додайте власний Exception Filter для узгодженого формату помилок.
Розбийте список користувачів на сторінки курсорною пагінацією.
Напишіть базові e2e-тести для основних ендпоінтів.
Опишіть Docker Compose для локального запуску API разом із PostgreSQL.
Пройдіть курс NestJS перед цим проєктом, якщо ще не робили цього, — кожен крок тут прямо посилається на конкретний урок курсу, а не пояснює механізм заново.
Підказки
Ніколи не повертайте хеш пароля користувача у відповіді API — навіть якщо фронтенду він не потрібен, випадковий зайвий вивід поля в JSON-відповіді є витоком чутливих даних.
AuthGuard перевіряє лише наявність і валідність токена; окрема перевірка ролі/прав (RolesGuard) — окремий, додатковий Guard, а не розширення того самого.
У Docker Compose база даних не повинна мати проброшений порт назовні, якщо до неї звертається лише сам API-контейнер усередині тієї самої мережі Compose (курс Docker, урок про networks).
src/prisma/prisma.module.ts
import { Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Module({
providers: [PrismaService],
exports: [PrismaService], // без exports інші модулі не змогли б впровадити PrismaService
})
export class PrismaModule {}Контролер делегує роботу сервісу, сервіс звертається до бази даних через PrismaService.
src/users/users.service.ts
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
findAll() {
return this.prisma.user.findMany();
}
findOne(id: string) {
return this.prisma.user.findUnique({ where: { id } });
}
create(data: { email: string; name: string }) {
return this.prisma.user.create({ data });
}
}src/users/users.controller.ts
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.usersService.findOne(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}Guard перевіряє JWT у заголовку Authorization перед допуском запиту до контролера.
Та сама механіка перевірки токена, що й у статті «JWT Authentication: як це працює», реалізована як Guard (курс NestJS, урок про Guards) — перевіряється й підпис, і термін дії, до того, як запит дійде до контролера:
src/auth/auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.replace("Bearer ", "");
if (!token || !this.isValidToken(token)) {
throw new UnauthorizedException("Потрібна автентифікація");
}
return true;
}
private isValidToken(token: string): boolean {
// верифікація підпису й терміну дії токена
return Boolean(token);
}
}src/users/users.controller.ts — фрагмент
@Controller("users")
@UseGuards(AuthGuard) // усі маршрути цього контролера вимагають дійсного токена
export class UsersController { /* ... */ }Тіло запиту перевіряється декларативно через ValidationPipe, а не ручними if-перевірками в контролері.
Та сама пара DTO + ValidationPipe, що й в уроці «Pipes: валідація та трансформація вхідних даних» курсу NestJS:
src/users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from "class-validator";
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
name: string;
}src/main.ts — фрагмент
app.useGlobalPipes(new ValidationPipe());Власний Exception Filter повертає узгоджений формат помилки для всього API.
Той самий підхід, що й в уроці «Exception Filters: власна обробка помилок» курсу NestJS — централізовано, а не в кожному контролері окремо:
src/common/http-exception.filter.ts
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(exception.getStatus()).json({
timestamp: new Date().toISOString(),
path: ctx.getRequest().url,
message: exception.message,
});
}
}Список користувачів розбитий на сторінки курсорною пагінацією — той самий підхід, що й у статті «Pagination: offset vs cursor».
Базові e2e-тести перевіряють, що основні ендпоінти повертають очікувані статус-коди й дані.
docker-compose.yml піднімає API та PostgreSQL разом однією командою.
Той самий двосервісний шаблон, що й у практичному прикладі курсу Docker («Практичний docker-compose.yml для веб-застосунку») — застосунок звертається до бази даних за іменем сервіса, а порт назовні проброшено лише для самого API:
docker-compose.yml
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://appuser:apppass@db:5432/appdb
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: