W ramach tej lekcji zajmiemy się stworzeniem kompletnego projektu, który pozwoli zastosować wszystkie poznane koncepcje NestJS w praktyce. Naszym celem będzie zbudowanie mini aplikacji API do zarządzania zadaniami. Projekt ten pozwoli lepiej zrozumieć, jak integrować różne elementy NestJS oraz jak zbudować funkcjonalną i wydajną aplikację serwerową.
Cele Projektu
- Zastosować wszystkie poznane wcześniej koncepcje NestJS, takie jak moduły, kontrolery, serwisy, autoryzacja, middleware i inne.
- Stworzyć mini aplikację – API do zarządzania zadaniami.
- Zrozumieć, jak wszystkie elementy współpracują ze sobą w pełnej aplikacji.
Krok 1: Przygotowanie Projektu
Na początek stworzymy nowy projekt NestJS, używając CLI:
npx @nestjs/cli new task-manager
Podczas tworzenia projektu NestJS zapyta o wybranie managera pakietów – wybieramy ten, który preferujemy, np. npm lub yarn.
Krok 2: Struktura Aplikacji
Nasza aplikacja do zarządzania zadaniami będzie mieć następującą strukturę:
- Moduły: oddzielają różne funkcjonalności (np. TaskModule).
- Kontrolery: zajmują się przetwarzaniem zapytań HTTP.
- Serwisy: logika biznesowa aplikacji.
- Baza danych: korzystamy z TypeORM i PostgreSQL do przechowywania zadań.
Utworzymy moduł zarządzania zadaniami:
nest generate module tasks
Następnie generujemy kontroler i serwis dla tego modułu:
nest generate controller tasks
nest generate service tasks
Krok 3: Tworzenie Modelu Task
Zacznijmy od stworzenia podstawowej struktury, która będzie reprezentować zadanie. Stworzymy klasę Task oraz połączymy ją z TypeORM, aby przechowywać zadania w bazie danych.
tasks/task.entity.ts
import {Column, Entity, PrimaryGeneratedColumn} from "typeorm";
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
@Column({ default: 'OPEN'})
status: string;
}
W tym kodzie:
- @Entity() – dekorator, który oznacza, że klasa jest encją przechowywaną w bazie danych.
- @PrimaryGeneratedColumn() – automatycznie generowane ID zadania.
- @Column() – kolumna, która będzie przechowywać dane (tytuł, opis, status).
Krok 4: Konfiguracja Bazy Danych
Kolejnym krokiem jest skonfigurowanie połączenia z bazą danych. Otwórz app.module.ts i dodaj konfigurację TypeORM:
app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';
import { Task } from './tasks/task.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'taskmanagement',
entities: [Task],
synchronize: true,
}),
TasksModule,
],
})
export class AppModule {}
Opis:
- TypeOrmModule.forRoot() – konfigurujemy połączenie z bazą danych.
- entities: [Task] – podajemy wszystkie encje, które będą używane.
- synchronize: true – automatyczna synchronizacja struktury bazy danych z kodem (używane w środowisku deweloperskim).
Krok 5: Tworzenie Serwisu TaskService
Przechodzimy do serwisu, który będzie zajmował się logiką zarządzania zadaniami, taką jak tworzenie, aktualizowanie, usuwanie.
tasks/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Task } from './task.entity';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private taskRepository: Repository,
) {}
findAll(): Promise {
return this.taskRepository.find();
}
findOne(id: number): Promise {
return this.taskRepository.findOneBy({ id });
}
async createTask(title: string, description: string): Promise {
const task = this.taskRepository.create({ title, description });
return await this.taskRepository.save(task);
}
async removeTask(id: number): Promise {
await this.taskRepository.delete(id);
}
}
Opis:
- @InjectRepository(Task) – używamy repozytorium do zarządzania danymi w bazie.
- findAll(), findOne(), createTask(), removeTask() – metody do wyszukiwania, tworzenia i usuwania zadań.
Krok 6: Tworzenie Kontrolera TaskController
Teraz utworzymy kontroler, który będzie przetwarzał żądania HTTP:
tasks/task.controller.ts
import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { Task } from './task.entity';
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
getAllTasks(): Promise {
return this.tasksService.findAll();
}
@Get(':id')
getTask(@Param('id') id: number): Promise {
return this.tasksService.findOne(id);
}
@Post()
createTask(
@Body('title') title: string,
@Body('description') description: string,
): Promise {
return this.tasksService.createTask(title, description);
}
@Delete(':id')
deleteTask(@Param('id') id: number): Promise {
return this.tasksService.removeTask(id);
}
}
Opis:
- @Controller(’tasks’) – kontroler zarządza ścieżką
/tasks. - @Get(), @Post(), @Delete() – dekoratory wskazujące, jaki rodzaj żądania HTTP jest przetwarzany.
- @Param(’id’) id: number – pobieramy parametry ścieżki, np. ID zadania.
- @Body() – służy do pobierania danych z ciała żądania.
Krok 7: Middleware i Guards
Dodajemy middleware do logowania wszystkich przychodzących żądań:
tasks/logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: () => void) {
console.log(`Request...`);
next();
}
}
Dodajemy middleware do aplikacji w tasks.module.ts:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Task } from './task.entity';
import { LoggingMiddleware } from './logging.middleware';
@Module({
imports: [TypeOrmModule.forFeature([Task])],
controllers: [TasksController],
providers: [TasksService],
})
export class TasksModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes(TasksController);
}
}
Krok 8: Dodanie Modułu User
Dodajmy odpowiednie pliki i zaktualizujmy konfigurację. Poniżej znajdziesz dokładną instrukcję.
1. Tworzenie Modułu User
Na początek generujemy moduł dla użytkownika (user). Możemy to zrobić poleceniem:
nest generate module user
Następnie generujemy serwis:
nest generate service user
Teraz zaimplementujemy pliki tak, jak opisałeś.
2. Implementacja User w plikach
user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
username: string;
@Column()
password: string;
}
Opis:
@Entity()– klasaUserjest encją zarządzaną przez TypeORM.@PrimaryGeneratedColumn()– automatycznie generowaneID.@Column()– kolumnyusernameipassword.
user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "./user.entity";
import { Repository } from "typeorm";
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository,
) {}
async findOne(username: string): Promise {
return this.userRepository.findOneBy({ username });
}
async createUser(username: string, password: string): Promise {
const user = this.userRepository.create({ username, password });
return await this.userRepository.save(user);
}
async removeUser(id: number): Promise {
await this.userRepository.delete(id);
}
}
Opis:
findOne(username: string)– metoda znajduje użytkownika na podstawie nazwy użytkownika.createUser(username: string, password: string)– tworzenie nowego użytkownika.removeUser(id: number)– usuwanie użytkownika na podstawieID.
user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from "./user.entity";
import { UserService } from './user.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
Opis:
imports: [TypeOrmModule.forFeature([User])]– moduł TypeORM dostaje dostęp do repozytoriumUser.exports: [UserService]– eksportowanieUserService, aby można było go używać w innych modułach.
3. Aktualizacja AppModule
Zaktualizujmy AppModule, aby zaimportować nowo utworzony moduł UserModule:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';
import { UserModule } from './user/user.module';
import { Task } from './tasks/task.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'taskmanagement',
entities: [Task],
synchronize: true,
}),
TasksModule,
UserModule,
],
})
export class AppModule {}
Krok 9: Dodanie JWT do Autoryzacji
JWT (JSON Web Token) jest standardem do wymiany informacji, który jest często używany do uwierzytelniania użytkowników i autoryzacji. Dzięki temu, tylko uwierzytelnieni użytkownicy będą mieli dostęp do określonych części API.
Aby zaimplementować JWT w NestJS, przejdziemy przez następujące kroki:
- Instalacja niezbędnych bibliotek
- Tworzenie modułu autoryzacji (AuthModule)
- Tworzenie serwisu do uwierzytelniania użytkowników
- Generowanie tokenów JWT dla zalogowanych użytkowników
- Zabezpieczanie endpointów za pomocą guardów
1. Instalacja Niezbędnych Bibliotek
Pierwszym krokiem jest instalacja paczek, które są potrzebne do obsługi JWT:
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcryptjs
npm install -D @types/passport-jwt @types/bcryptjs
Opis:
- @nestjs/jwt i @nestjs/passport – biblioteki do obsługi JWT w NestJS.
- passport i passport-jwt – narzędzie do autoryzacji.
- bcryptjs – do hashowania haseł.
2. Tworzenie Modułu Autoryzacji (AuthModule)
Stworzymy moduł autoryzacji, który będzie odpowiedzialny za logowanie użytkowników i zarządzanie autoryzacją. Najpierw generujemy nowy moduł:
nest generate module auth
3. Tworzenie Serwisu do Uwierzytelniania Użytkowników
Następnie stworzymy serwis do zarządzania uwierzytelnianiem. Serwis ten będzie odpowiedzialny za sprawdzanie użytkowników, logowanie oraz generowanie tokenów JWT.
nest generate service auth
auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import * as bcrypt from 'bcryptjs';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise {
const user = await this.usersService.findOne(username);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId };
return {
access_token: this.jwtService.sign(payload),
};
}
}
Opis:
- validateUser(username: string, password: string) – metoda sprawdzająca, czy użytkownik istnieje oraz czy hasło jest poprawne (używając bcrypt).
- login(user: any) – metoda generująca token JWT. Token zawiera informacje, takie jak nazwa użytkownika oraz identyfikator.
4. Tworzenie Kontrolera Autoryzacji
Stwórzmy kontroler do obsługi operacji związanych z uwierzytelnianiem:
auth.controller.ts
import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@UseGuards(LocalAuthGuard)
@Post('login')
async login(@Request() req) {
return this.authService.login(req.user);
}
}
Opis:
- @UseGuards(LocalAuthGuard) – używamy guarda do zabezpieczenia tego endpointu. Guard sprawdza, czy dane logowania są poprawne.
- @Post(’login’) – punkt końcowy
/auth/loginprzyjmujący dane logowania.
5. Tworzenie LocalAuthGuard
LocalAuthGuard jest guardem, który obsługuje uwierzytelnianie przy użyciu danych użytkownika. Korzysta z mechanizmu Passport do sprawdzania danych:
local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
6. Konfiguracja Passport Strategy
Musimy zdefiniować strategię logowania lokalnego, używając Passport:
local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Opis:
- LocalStrategy używa Passport do obsługi uwierzytelniania.
- validate() sprawdza, czy podane dane użytkownika są poprawne, a jeśli nie, zwraca wyjątek
UnauthorizedException.
7. Tworzenie JWT Strategy
JWT Strategy pozwala na zweryfikowanie poprawności tokenu JWT, kiedy użytkownik próbuje uzyskać dostęp do chronionych zasobów:
jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
Opis:
- jwtFromRequest – pobieramy token JWT z nagłówka
Authorization(typ Bearer). - secretOrKey – klucz do weryfikacji tokenu (w środowisku produkcyjnym należy używać zmiennych środowiskowych do przechowywania kluczy).
- validate(payload: any) – metoda służy do weryfikacji użytkownika na podstawie payloadu z tokenu.
8. Zabezpieczanie Endpointów
Ostatecznym krokiem jest zabezpieczenie endpointów za pomocą JWT Guards. Dodamy JwtAuthGuard do kontrolera TasksController, aby dostęp do operacji związanych z zadaniami był możliwy tylko dla zalogowanych użytkowników.
jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
tasks/task.controller.ts
import { Controller, Get, Post, Delete, Param, Body, UseGuards } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('tasks')
@UseGuards(JwtAuthGuard) // Używamy JwtAuthGuard do zabezpieczenia
export class TasksController {
constructor(private readonly tasksService: TasksService) {}
@Get()
getAllTasks() {
return this.tasksService.findAll();
}
@Post()
createTask(
@Body('title') title: string,
@Body('description') description: string,
) {
return this.tasksService.createTask(title, description);
}
@Delete(':id')
deleteTask(@Param('id') id: number) {
return this.tasksService.removeTask(id);
}
}
Opis:
- @UseGuards(JwtAuthGuard) – zabezpieczamy wszystkie metody kontrolera
TasksControllerprzy użyciu JWT. - Tylko użytkownicy posiadający poprawny token będą mogli uzyskać dostęp do zasobów.
9. Dodanie JwtStrategy do AuthModule
Dodanie JwtStrategy do providers w AuthModule jest kluczowe, aby odpowiednio skonfigurować autoryzację JWT w aplikacji. Upewniamy się, że strategia JWT będzie zarejestrowana i gotowa do użycia w celu weryfikacji tokenów.
Oto jak powinien wyglądać zaktualizowany AuthModule:
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'secretKey', // Powinno być przechowywane jako zmienna środowiskowa w środowisku produkcyjnym
signOptions: { expiresIn: '60m' },
}),
UserModule,
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
Kluczowe Zmiany
Dodanie
JwtStrategydoproviders:JwtStrategydodajemy doproviders, aby NestJS mógł używać tej strategii w mechanizmach Passport.- Jest to kluczowe, ponieważ
JwtStrategyobsługuje weryfikację tokenów JWT podczas uwierzytelniania i autoryzacji użytkowników.
Konfiguracja
JwtModule:JwtModulez@nestjs/jwtjest zarejestrowany zsecretorazsignOptions, aby możliwe było generowanie i weryfikowanie tokenów.- Wartość
secretpowinna być przechowywana jako zmienna środowiskowa, aby uniknąć wycieku klucza prywatnego.
Krok 10: Testowanie Aplikacji z JWT
Testowanie aplikacji z JWT wymaga nie tylko testów jednostkowych, ale także integracyjnych, aby sprawdzić, czy użytkownicy mogą się zalogować i uzyskać dostęp do chronionych zasobów za pomocą tokenu JWT.
a) Testy Jednostkowe dla Serwisu AuthService
Rozpoczniemy od testowania serwisu AuthService, który jest odpowiedzialny za walidację użytkownika oraz generowanie tokenów.
auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
describe('AuthService', () => {
let authService: AuthService;
let usersService: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: {
findOne: jest.fn(),
},
},
{
provide: JwtService,
useValue: {
sign: jest.fn().mockReturnValue('fake-jwt-token'),
},
},
],
}).compile();
authService = module.get(AuthService);
usersService = module.get(UsersService);
});
it('should validate user successfully', async () => {
const user = { userId: 1, username: 'john', password: await bcrypt.hash('changeme', 10) };
jest.spyOn(usersService, 'findOne').mockResolvedValue(user);
const result = await authService.validateUser('john', 'changeme');
expect(result).toEqual({ userId: 1, username: 'john' });
});
it('should return null if user validation fails', async () => {
jest.spyOn(usersService, 'findOne').mockResolvedValue(null);
const result = await authService.validateUser('john', 'changeme');
expect(result).toBeNull();
});
it('should generate a JWT token for the user', async () => {
const user = { userId: 1, username: 'john' };
const result = await authService.login(user);
expect(result.access_token).toEqual('fake-jwt-token');
});
});
Opis:
- validateUser() – sprawdzamy, czy użytkownik z odpowiednim hasłem może być poprawnie zwalidowany.
- login() – sprawdzamy, czy poprawnie generujemy token JWT dla zalogowanego użytkownika.
b) Testy Integracyjne Dla AuthController
Testowanie kontrolera AuthController jest ważne, aby upewnić się, że proces logowania działa poprawnie i generuje poprawny token JWT.
auth/auth.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
describe('AuthController', () => {
let authController: AuthController;
let authService: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{
provide: AuthService,
useValue: {
login: jest.fn().mockResolvedValue({ access_token: 'fake-jwt-token' }),
},
},
],
}).compile();
authController = module.get(AuthController);
authService = module.get(AuthService);
});
it('should login and return JWT token', async () => {
const req = {
user: { userId: 1, username: 'john' },
};
const result = await authController.login(req);
expect(result.access_token).toEqual('fake-jwt-token');
});
});
Opis:
- login() – sprawdzamy, czy metoda zwraca poprawnie wygenerowany token dla użytkownika, symulując logowanie.
c) Testy Integracyjne Endpointów TasksController
Kolejnym krokiem jest sprawdzenie, czy endpointy w TasksController są zabezpieczone i można je wywołać tylko wtedy, gdy użytkownik posiada odpowiedni token JWT.
tasks/tasks.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TasksController } from './tasks.controller';
import { TasksService } from './tasks.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
describe('TasksController', () => {
let tasksController: TasksController;
let tasksService: TasksService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [PassportModule],
controllers: [TasksController],
providers: [
TasksService,
JwtService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
}).compile();
tasksController = module.get(TasksController);
tasksService = module.get(TasksService);
});
it('should be defined', () => {
expect(tasksController).toBeDefined();
});
it('should get all tasks', async () => {
const result = [{ id: 1, title: 'Test Task', description: 'Test Desc', status: 'OPEN' }];
jest.spyOn(tasksService, 'findAll').mockResolvedValue(result);
expect(await tasksController.getAllTasks()).toBe(result);
});
it('should protect routes with JWT', async () => {
// Testujemy, czy wymagane jest posiadanie tokenu JWT.
// To jest raczej teoretyczny test, ponieważ rzeczywista ochrona jest realizowana przez NestJS i Passport.
// Testujemy, czy JwtAuthGuard jest odpowiednio skonfigurowany.
expect(JwtAuthGuard).toBeDefined();
});
});
Opis:
- getAllTasks() – sprawdzamy, czy metoda zwraca wszystkie zadania. Zakładamy, że użytkownik posiada odpowiedni token.
- should protect routes with JWT – sprawdzamy, czy guard jest zainicjowany, aby chronić zasoby.
d) Testowanie JWT Guard
Na koniec przetestujemy JwtAuthGuard, aby upewnić się, że użytkownicy bez tokenu nie będą mogli uzyskać dostępu do chronionych zasobów:
auth/jwt-auth.guard.spec.ts
import { JwtAuthGuard } from './jwt-auth.guard';
import { ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
describe('JwtAuthGuard', () => {
let jwtAuthGuard: JwtAuthGuard;
beforeEach(() => {
jwtAuthGuard = new JwtAuthGuard(new Reflector());
});
it('should be defined', () => {
expect(jwtAuthGuard).toBeDefined();
});
it('should return false if no authorization header', () => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({
headers: {},
}),
}),
} as ExecutionContext;
expect(jwtAuthGuard.canActivate(mockContext)).toBe(false);
});
});
Opis:
- should return false if no authorization header – ten test sprawdza, czy guard poprawnie blokuje dostęp, gdy nie jest dostarczony token JWT.
Podsumowanie Testów z JWT
Dodanie JWT wprowadza nowe wyzwania związane z testowaniem aplikacji. Aby zapewnić pełne pokrycie, przetestowaliśmy:
- Serwis
AuthService– testy jednostkowe, które sprawdzają poprawność walidacji użytkownika oraz generowania tokenów. - Kontroler
AuthController– testowanie endpointu logowania. - Kontroler
TasksController– testy zabezpieczenia chronionych zasobów oraz weryfikacja, czy dostęp jest ograniczony do użytkowników z tokenem JWT. - JwtAuthGuard – upewniliśmy się, że guard poprawnie blokuje nieautoryzowane żądania.
Dzięki temu podejściu możemy mieć pewność, że autoryzacja użytkowników jest realizowana w sposób bezpieczny, a tylko zalogowani użytkownicy mają dostęp do chronionych zasobów.
Krok 11: Obsługa Błędów
Każda aplikacja powinna posiadać zaimplementowaną solidną obsługę błędów, która umożliwi użytkownikowi otrzymywanie odpowiednich komunikatów. Dodanie Globalnego Filtra dla wyjątków (Exception Filter) pomoże w eleganckiej obsłudze błędów.
tasks/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
Dodanie filtra w app.module.ts:
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './tasks/http-exception.filter';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
export class AppModule {}
Krok 12: Dokumentacja API za pomocą Swaggera
Dokumentacja API to kluczowy element każdej aplikacji. Możemy użyć Swaggera, aby wygenerować interaktywną dokumentację.
app.module.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Task Manager')
.setDescription('API do zarządzania zadaniami')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
Krok 13: Optymalizacja Wydajności
Możemy również zadbać o optymalizację wydajności aplikacji, stosując odpowiednie techniki, takie jak caching za pomocą Redis:
tasks/task.service.ts
Dodanie mechanizmu cachowania do metody wyszukiwania wszystkich zadań:
import { CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class TasksService {
constructor(
@InjectRepository(Task)
private taskRepository: Repository,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async findAll(): Promise {
const cachedTasks = await this.cacheManager.get('tasks');
if (cachedTasks) {
return cachedTasks;
}
const tasks = await this.taskRepository.find();
await this.cacheManager.set('tasks', tasks, { ttl: 1000 });
return tasks;
}
}
Krok 14: Bezpieczeństwo
Dodanie narzędzi zabezpieczających, takich jak helmet do ochrony przed atakami XSS, oraz rate limiter do zabezpieczenia przed zbyt wieloma zapytaniami:
main.ts
import * as helmet from 'helmet';
import * as rateLimit from 'express-rate-limit';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet()); // Zabezpieczenie nagłówków
app.use(
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minut
max: 100, // limit na 100 zapytań na IP
}),
);
await app.listen(3000);
}
bootstrap();
Krok 15: Zabezpieczenie Endpoints za pomocą Guards
Dodanie guardów, aby upewnić się, że tylko zalogowani użytkownicy mają dostęp do określonych endpointów:
auth/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Dodatkowe logiki zabezpieczeń
return super.canActivate(context);
}
}
tasks/task.controller.ts
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('tasks')
@UseGuards(JwtAuthGuard)
export class TasksController {
// wszystkie endpointy w tym kontrolerze będą zabezpieczone przez JwtAuthGuard
}
Krok 16: Integracja z Front-Endem
Jeśli tworzysz aplikację full-stack, warto zintegrować API z frontendem, np. React lub Angular, aby w pełni zademonstrować możliwości aplikacji.
- Dodaj endpointy do uwierzytelniania użytkowników (np. rejestracja, logowanie).
- Skonfiguruj CORS, aby umożliwić komunikację pomiędzy backendem a frontendem.
Krok 17: Deployment Aplikacji
Na zakończenie wdrażamy aplikację na chmurze – możemy skorzystać z Dockera oraz chmury, takiej jak AWS, DigitalOcean czy Heroku.
Dockerfile
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start:prod"]
Krok 18: Monitorowanie i Utrzymanie
Wdrożenie aplikacji produkcyjnej wiąże się z koniecznością monitorowania jej działania i stabilności:
- Sentry do śledzenia błędów.
- Prometheus i Grafana do monitorowania wydajności.
Integracja Sentry:
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
});
Podsumowanie
Projekt końcowy w NestJS nie musi ograniczać się tylko do kilku kluczowych punktów. Rozszerzenie o dodatkowe aspekty, takie jak testowanie, optymalizacja, dokumentacja, bezpieczeństwo oraz monitorowanie aplikacji, pozwala na pełniejsze zrozumienie, jak zbudować kompletną, profesjonalną aplikację serwerową.
Każdy z kroków opisanych w tym artykule pokazuje, jak praktycznie wdrożyć zdobyte wcześniej umiejętności, aby stworzyć aplikację, która jest wydajna, bezpieczna i łatwa do utrzymania. Dzięki zastosowaniu tych technik możemy przejść od prostego API do pełnoprawnej aplikacji, która spełnia wymogi produkcyjne.