Przejdź do treści
Porównania

Express.js vs NestJS - Który framework Node.js wybrać?

Opublikowano:
·
Zaktualizowano:
·6 min czytania·Autor: MDS Software Solutions Group

Express.js NestJS Który

porownania

Express.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.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

Express.js vs NestJS - Który framework Node.js wybrać? | MDS Software Solutions Group | MDS Software Solutions Group