Express.js vs NestJS - Który framework Node.js wybrać?
Express.js NestJS Który
porownaniaExpress.js vs NestJS - Kompleksowe Porównanie Frameworków Backend Node.js
Wybór odpowiedniego frameworka backendowego to jedna z najważniejszych decyzji architektonicznych w projekcie. W ekosystemie Node.js dwa frameworki dominują rynek: Express.js - lekki, minimalistyczny weteran, oraz NestJS - nowoczesny, pełnowartościowy framework inspirowany Angularem. W tym artykule szczegółowo porównamy oba rozwiązania, abyś mógł podjąć świadomą decyzję.
Express.js - Minimalistyczne Podejście do Backendu#
Czym jest Express.js?#
Express.js to minimalistyczny framework webowy dla Node.js, który od lat pozostaje najpopularniejszym wyborem do tworzenia serwerów HTTP i API. Powstał w 2010 roku i od tego czasu zyskał ogromną bazę użytkowników. Jego filozofia opiera się na prostocie - dostarcza absolutne minimum, pozwalając deweloperom budować aplikację według własnych preferencji.
Kluczowe cechy Express.js#
- Minimalizm - brak narzuconej struktury projektu
- Middleware - elastyczny system przetwarzania żądań
- Ogromny ekosystem - tysiące paczek middleware na npm
- Niska bariera wejścia - prostota API pozwala szybko zacząć
- Pełna kontrola - deweloper decyduje o każdym aspekcie architektury
Podstawowy serwer Express.js#
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// Logika tworzenia użytkownika
res.status(201).json({ id: 1, name, email });
});
app.listen(3000, () => {
console.log('Serwer Express działa na porcie 3000');
});
NestJS - Architektura Inspirowana Angularem#
Czym jest NestJS?#
NestJS to progresywny framework Node.js do budowania wydajnych, skalowalnych aplikacji serwerowych. Stworzony przez Kamila Mysliwca w 2017 roku, czerpie inspirację z Angulara, wykorzystując dekoratory, moduły i wstrzykiwanie zależności. Pod spodem domyślnie używa Express.js (lub opcjonalnie Fastify), dodając warstwę abstrakcji i struktury.
Kluczowe cechy NestJS#
- Modularna architektura - organizacja kodu w moduły
- Wstrzykiwanie zależności - wbudowany kontener IoC
- Dekoratory - czytelna, deklaratywna składnia
- Natywne wsparcie TypeScript - napisany w TypeScript od podstaw
- Bogate CLI - generowanie komponentów z linii poleceń
- Wbudowane wsparcie dla WebSockets, GraphQL, mikrousług
Podstawowy kontroler NestJS#
import { Controller, Get, Post, Body, HttpCode } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('api/users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Post()
@HttpCode(201)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}
Porównanie Architektury#
Express.js - Podejście Middleware#
Express.js opiera się na wzorcu middleware - funkcji, które przetwarzają żądania HTTP w określonej kolejności. Każda funkcja middleware ma dostęp do obiektu żądania (req), odpowiedzi (res) oraz funkcji next().
import express, { Request, Response, NextFunction } from 'express';
// Middleware logowania
const logger = (req: Request, res: Response, next: NextFunction) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
};
// Middleware autoryzacji
const auth = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Brak tokenu autoryzacji' });
}
// Weryfikacja tokenu
next();
};
const app = express();
app.use(logger);
app.use('/api/protected', auth);
Ta elastyczność jest jednocześnie zaletą i wadą - nie ma narzuconej struktury, co w dużych projektach może prowadzić do chaosu.
NestJS - Moduły, Kontrolery i Serwisy#
NestJS narzuca jasną strukturę opartą na trzech filarach: modułach, kontrolerach i serwisach (providerach). Każda funkcjonalność jest enkapsulowana w module.
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { User } from './entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.usersRepository.find();
}
async findOne(id: number): Promise<User> {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`Użytkownik o ID ${id} nie istnieje`);
}
return user;
}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}
Wsparcie dla TypeScript#
Express.js i TypeScript#
Express.js został napisany w JavaScript, a wsparcie dla TypeScript wymaga dodatkowej konfiguracji. Typy są dostępne przez paczkę @types/express, ale nie zawsze pokrywają wszystkie scenariusze.
import express, { Request, Response } from 'express';
interface UserRequest extends Request {
body: {
name: string;
email: string;
password: string;
};
}
interface UserResponse {
id: number;
name: string;
email: string;
}
app.post('/api/users', (req: UserRequest, res: Response<UserResponse>) => {
const { name, email, password } = req.body;
// Brak walidacji na poziomie frameworka
const user: UserResponse = { id: 1, name, email };
res.json(user);
});
NestJS i TypeScript#
NestJS jest napisany natywnie w TypeScript i w pełni wykorzystuje jego możliwości. Dekoratory, generyki i zaawansowane typy są integralną częścią frameworka.
// create-user.dto.ts
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsString()
avatar?: string;
}
// Walidacja działa automatycznie z ValidationPipe
// main.ts
import { ValidationPipe } from '@nestjs/common';
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));
Wstrzykiwanie Zależności (Dependency Injection)#
Express.js - Brak Wbudowanego DI#
Express.js nie posiada wbudowanego mechanizmu wstrzykiwania zależności. Deweloperzy muszą samodzielnie zarządzać zależnościami lub korzystać z zewnętrznych bibliotek jak tsyringe czy inversify.
// Ręczne zarządzanie zależnościami w Express
import { UserRepository } from './repositories/UserRepository';
import { UserService } from './services/UserService';
import { EmailService } from './services/EmailService';
// Ręczne tworzenie instancji i łączenie zależności
const userRepository = new UserRepository(database);
const emailService = new EmailService(smtpConfig);
const userService = new UserService(userRepository, emailService);
app.get('/api/users', async (req, res) => {
const users = await userService.findAll();
res.json(users);
});
NestJS - Wbudowany Kontener IoC#
NestJS posiada potężny, wbudowany kontener Inversion of Control (IoC), który automatycznie rozwiązuje zależności.
// NestJS automatycznie zarządza zależnościami
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly emailService: EmailService,
private readonly cacheService: CacheService,
) {}
// Wszystkie zależności są automatycznie wstrzykiwane
}
// Rejestracja w module
@Module({
providers: [
UserService,
UserRepository,
EmailService,
CacheService,
],
})
export class UserModule {}
Porównanie Routingu#
Routing w Express.js#
import { Router } from 'express';
const router = Router();
// Parametry w URL
router.get('/users/:id', async (req, res) => {
const { id } = req.params;
const user = await userService.findById(parseInt(id));
res.json(user);
});
// Query parameters
router.get('/users', async (req, res) => {
const { page = '1', limit = '10', search } = req.query;
const users = await userService.findAll({
page: parseInt(page as string),
limit: parseInt(limit as string),
search: search as string,
});
res.json(users);
});
// Grupowanie tras
const apiRouter = Router();
apiRouter.use('/users', router);
apiRouter.use('/products', productRouter);
app.use('/api/v1', apiRouter);
Routing w NestJS#
import {
Controller, Get, Post, Put, Delete,
Param, Query, Body, ParseIntPipe,
DefaultValuePipe, HttpStatus
} from '@nestjs/common';
@Controller('api/v1/users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
@Query('search') search?: string,
) {
return this.usersService.findAll({ page, limit, search });
}
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}
NestJS oferuje wbudowane pipe'y do transformacji i walidacji parametrów, co eliminuje konieczność ręcznego parsowania typów.
Integracja z Bazami Danych#
Express.js z Prisma#
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
app.get('/api/users', async (req, res) => {
try {
const users = await prisma.user.findMany({
include: { posts: true },
orderBy: { createdAt: 'desc' },
});
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Błąd serwera' });
}
});
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({
data: req.body,
});
res.status(201).json(user);
} catch (error) {
if (error.code === 'P2002') {
res.status(409).json({ error: 'Email już istnieje' });
}
res.status(500).json({ error: 'Błąd serwera' });
}
});
NestJS z TypeORM#
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, OneToMany, CreateDateColumn } from 'typeorm';
import { Post } from '../posts/post.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
@CreateDateColumn()
createdAt: Date;
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepo: Repository<User>,
) {}
async findAll(): Promise<User[]> {
return this.usersRepo.find({
relations: ['posts'],
order: { createdAt: 'DESC' },
});
}
async create(dto: CreateUserDto): Promise<User> {
const exists = await this.usersRepo.findOne({
where: { email: dto.email },
});
if (exists) {
throw new ConflictException('Email już istnieje');
}
const user = this.usersRepo.create(dto);
return this.usersRepo.save(user);
}
}
Oba frameworki świetnie współpracują z popularnymi ORM-ami. NestJS oferuje jednak dedykowane moduły integracyjne (@nestjs/typeorm, @nestjs/mongoose, @nestjs/prisma), które ułatwiają konfigurację.
Autentykacja i Autoryzacja#
Express.js z Passport.js#
import passport from 'passport';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
}, async (payload, done) => {
try {
const user = await userService.findById(payload.sub);
if (!user) return done(null, false);
return done(null, user);
} catch (error) {
return done(error, false);
}
}));
// Middleware autoryzacji ról
const requireRole = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Brak uprawnień' });
}
next();
};
};
app.get('/api/admin/users',
passport.authenticate('jwt', { session: false }),
requireRole('admin'),
async (req, res) => {
const users = await userService.findAll();
res.json(users);
}
);
NestJS z Guards i Decorators#
// jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// roles.decorator.ts
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// Użycie w kontrolerze
@Controller('api/admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles(Role.Admin)
findAllUsers() {
return this.usersService.findAll();
}
@Get('stats')
@Roles(Role.Admin, Role.Manager)
getStats() {
return this.statsService.getDashboard();
}
}
NestJS oferuje bardziej deklaratywne i czytelne podejście do autoryzacji dzięki dekoratorom i guardom.
Strategie Testowania#
Testowanie Express.js#
import request from 'supertest';
import express from 'express';
describe('Users API', () => {
let app: express.Application;
beforeEach(() => {
app = express();
app.use(express.json());
// Konfiguracja tras
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: 'Jan' }]);
});
});
it('powinien zwrócić listę użytkowników', async () => {
const response = await request(app)
.get('/api/users')
.expect(200);
expect(response.body).toHaveLength(1);
expect(response.body[0].name).toBe('Jan');
});
it('powinien utworzyć użytkownika', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Anna', email: 'anna@example.com' })
.expect(201);
expect(response.body.name).toBe('Anna');
});
});
Testowanie NestJS#
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
findAll: jest.fn().mockResolvedValue([
{ id: 1, name: 'Jan', email: 'jan@example.com' },
]),
create: jest.fn().mockImplementation((dto) =>
Promise.resolve({ id: 2, ...dto }),
),
},
},
],
}).compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('powinien zwrócić listę użytkowników', async () => {
const result = await controller.findAll();
expect(result).toHaveLength(1);
expect(service.findAll).toHaveBeenCalled();
});
it('powinien utworzyć użytkownika', async () => {
const dto = { name: 'Anna', email: 'anna@example.com', password: '12345678' };
const result = await controller.create(dto);
expect(result.name).toBe('Anna');
expect(service.create).toHaveBeenCalledWith(dto);
});
});
NestJS z wbudowanym modułem testowym (@nestjs/testing) znacząco ułatwia unit testy dzięki automatycznemu mockingowi zależności.
Porównanie Wydajności#
Pod względem surowej wydajności Express.js jest nieznacznie szybszy, ponieważ NestJS dodaje warstwę abstrakcji. Jednak różnice w praktyce są minimalne:
| Metryka | Express.js | NestJS (Express) | NestJS (Fastify) | |---------|-----------|-------------------|-------------------| | Żądania/s (proste GET) | ~15 000 | ~13 500 | ~28 000 | | Latencja (p95) | ~2.1ms | ~2.5ms | ~1.2ms | | Zużycie pamięci (idle) | ~35 MB | ~55 MB | ~50 MB | | Czas startu | ~100ms | ~500ms | ~450ms |
Warto zauważyć, że NestJS z adapterem Fastify znacząco przewyższa obie konfiguracje oparte na Express pod względem szybkości.
Społeczność i Ekosystem#
| Aspekt | Express.js | NestJS | |--------|-----------|--------| | Gwiazdy GitHub | ~65 000 | ~68 000 | | Pobrania npm (tyg.) | ~30 mln | ~3.5 mln | | Rok powstania | 2010 | 2017 | | Paczki middleware | Tysiące | Setki (+ paczki Express) | | Oficjalne integracje | Brak | TypeORM, Mongoose, GraphQL, WebSockets, gRPC | | Dokumentacja | Dobra, ale minimalna | Bardzo rozbudowana |
Express.js ma znacznie większą bazę użytkowników i więcej zasobów edukacyjnych. NestJS szybko rośnie i oferuje bogatszą oficjalną dokumentację z przykładami.
Krzywa Uczenia się#
Express.js#
- Początek: Bardzo prosty - kilka linii kodu wystarczy do uruchomienia serwera
- Średni poziom: Wymaga samodzielnego poznania wzorców architektonicznych
- Zaawansowany: Trzeba znać najlepsze praktyki z doświadczenia, bo framework ich nie narzuca
NestJS#
- Początek: Bardziej wymagający - trzeba zrozumieć moduły, kontrolery, serwisy, dekoratory i DI
- Średni poziom: Po zrozumieniu koncepcji, rozwój jest bardzo produktywny
- Zaawansowany: Bogata dokumentacja i jasne wzorce ułatwiają skalowanie
Kiedy Wybrać Express.js?#
- Małe projekty i prototypy - szybki start bez zbędnej złożoności
- Mikroserwisy - lekki footprint idealny dla prostych usług
- Maksymalna elastyczność - chcesz sam decydować o każdym aspekcie architektury
- Doświadczony zespół - potrafi samodzielnie zbudować dobrą architekturę
- Istniejący projekt - migracja z Express do NestJS jest kosztowna
- Proste API - CRUD bez złożonej logiki biznesowej
Kiedy Wybrać NestJS?#
- Duże projekty enterprise - narzucona struktura zapewnia spójność
- Zespoły - jasne konwencje ułatwiają współpracę
- Złożona logika biznesowa - DI i modularność pomagają zarządzać złożonością
- TypeScript-first - natywne wsparcie bez kompromisów
- Mikrousługi - wbudowane wsparcie dla wielu transportów
- GraphQL - dedykowany moduł z dekoratorami i code-first podejściem
- Nowy projekt - jeśli zaczynasz od zera, NestJS daje solidne fundamenty
Tabela Porównawcza#
| Cecha | Express.js | NestJS | |-------|-----------|--------| | Architektura | Minimalistyczna, middleware | Modularna, MVC/CQRS | | TypeScript | Przez @types | Natywny | | Dependency Injection | Brak (zewnętrzne lib) | Wbudowany | | Walidacja | Zewnętrzne biblioteki | Wbudowane pipe'y + class-validator | | CLI | express-generator (podstawowe) | @nestjs/cli (rozbudowane) | | Testowanie | Ręczna konfiguracja | Wbudowany moduł testowy | | WebSockets | socket.io (zewnętrzne) | Wbudowane gateway'e | | GraphQL | apollo-server (zewnętrzne) | @nestjs/graphql (dedykowane) | | Mikrousługi | Ręczna implementacja | Wbudowane transporty | | Dokumentacja API | swagger-ui-express | @nestjs/swagger (automatyczna) | | Krzywa uczenia | Niska | Średnia-Wysoka | | Wydajność | Bardzo dobra | Dobra (z Fastify - doskonała) | | Skalowalność kodu | Zależy od dewelopera | Wbudowana przez architekturę |
Podsumowanie#
Zarówno Express.js, jak i NestJS to doskonałe frameworki, ale służą różnym celom. Express.js jest idealny, gdy potrzebujesz lekkiego, elastycznego rozwiązania i masz doświadczenie w budowaniu architektury. NestJS sprawdza się najlepiej w dużych projektach, gdzie struktura, testowalność i skalowalność kodu są priorytetem.
Trend w branży wyraźnie wskazuje na rosnącą popularność NestJS, szczególnie w projektach korporacyjnych. Jednak Express.js pozostaje niezastąpiony w scenariuszach wymagających minimalnego narzutu i maksymalnej elastyczności.
Potrzebujesz Profesjonalnego Backendu Node.js?#
W MDS Software Solutions Group specjalizujemy się w projektowaniu i wdrażaniu skalowalnych aplikacji backendowych zarówno w Express.js, jak i NestJS. Nasz zespół pomoże Ci wybrać optymalne rozwiązanie i zbudować solidną architekturę dopasowaną do potrzeb Twojego biznesu.
Skontaktuj się z nami, aby omówić Twój projekt - od konsultacji architektonicznej po pełne wdrożenie.
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.