NestJS - Pytania Rekrutacyjne dla Node.js Backend Developera [2026]

Sławomir Plamowski 14 min czytania
api backend dependency-injection nestjs nodejs typescript

NestJS to jeden z najpopularniejszych frameworków do budowy aplikacji backendowych w Node.js. Jeśli przygotowujesz się do rozmowy na stanowisko Node.js Backend Developer, ten przewodnik zawiera 42 pytania rekrutacyjne z odpowiedziami - od podstaw po zaawansowane tematy.

Spis treści


Podstawy NestJS

Czym jest NestJS i jakie problemy rozwiązuje?

Odpowiedź w 30 sekund: NestJS to progresywny framework Node.js do budowy wydajnych, skalowalnych aplikacji serwerowych. Wykorzystuje TypeScript, architekturę modularną inspirowaną Angular i wzorce jak Dependency Injection. Rozwiązuje główny problem Express.js - brak narzuconej struktury i architektury.

Odpowiedź w 2 minuty: NestJS powstał, aby rozwiązać problem "architektonicznego chaosu" w aplikacjach Node.js. Express.js jest świetny, ale nie narzuca żadnej struktury - każdy projekt wygląda inaczej.

NestJS wprowadza:

  • Modularną architekturę - jasny podział na moduły, kontrolery, serwisy
  • Dependency Injection - wbudowany kontener IoC
  • TypeScript first - pełne wsparcie dla typów
  • Dekoratory - czytelna konfiguracja przez @Module, @Controller, @Injectable
  • Abstrakcja HTTP - działa z Express lub Fastify pod spodem
// Przykład struktury NestJS
@Module({
  imports: [DatabaseModule],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

NestJS jest idealny dla:

  • Enterprise applications
  • Microservices
  • GraphQL APIs
  • Real-time applications (WebSockets)

Jaka jest różnica między NestJS a Express.js?

Odpowiedź w 30 sekund: Express.js to minimalistyczny framework bez narzuconej struktury. NestJS to pełnoprawny framework budowany na Express (lub Fastify), który dodaje architekturę modularną, Dependency Injection, TypeScript i wzorce enterprise. Express daje wolność, NestJS daje strukturę.

Odpowiedź w 2 minuty: Najlepiej zobrazować różnice w formie porównania tabelarycznego - poniżej zestawienie kluczowych cech obu frameworków:

Cecha Express.js NestJS
Architektura Brak narzuconej Modularna, opinionated
TypeScript Opcjonalny Native
DI Container Brak Wbudowany
Struktura projektu Dowolna Zdefiniowana
Krzywa uczenia Niska Średnia
Skalowanie Wymaga planowania Wbudowane wzorce
// Express.js
app.get('/users', (req, res) => {
  res.json(users);
});

// NestJS
@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

NestJS pod spodem używa Express (domyślnie) lub Fastify, więc możesz korzystać z całego ekosystemu middleware Express.


Moduły i Architektura

Czym jest moduł w NestJS i jaką rolę pełni?

Odpowiedź w 30 sekund: Moduł to podstawowa jednostka organizacyjna w NestJS, oznaczona dekoratorem @Module(). Grupuje powiązane kontrolery i providery, definiuje zależności (imports) i eksporty. Każda aplikacja ma przynajmniej jeden moduł główny (AppModule).

Odpowiedź w 2 minuty: Moduły w NestJS organizują aplikację w spójne bloki funkcjonalne. Każdy moduł enkapsuluje swoją logikę i może być importowany przez inne moduły.

@Module({
  imports: [DatabaseModule, AuthModule],  // Zależności
  controllers: [UsersController],          // Kontrolery
  providers: [UsersService, UsersRepository], // Serwisy
  exports: [UsersService],                 // Udostępnione na zewnątrz
})
export class UsersModule {}

Właściwości @Module():

  • imports - moduły, których providerów potrzebujemy
  • controllers - kontrolery obsługujące żądania HTTP
  • providers - serwisy, repozytoria, factory, itp.
  • exports - providery dostępne dla innych modułów

Struktura aplikacji:

src/
├── app.module.ts        # Root module
├── users/
│   ├── users.module.ts
│   ├── users.controller.ts
│   └── users.service.ts
├── auth/
│   ├── auth.module.ts
│   └── auth.service.ts

Czym są moduły dynamiczne i kiedy ich używać?

Odpowiedź w 30 sekund: Moduły dynamiczne to moduły konfigurowane w runtime przez metody statyczne (np. forRoot(), forRootAsync()). Używane gdy moduł wymaga konfiguracji - np. ConfigModule z różnymi opcjami, DatabaseModule z connection string.

Odpowiedź w 2 minuty: Moduły dynamiczne pozwalają na elastyczną konfigurację w czasie uruchomienia aplikacji. Poniżej przykład implementacji modułu z metodami forRoot() i forRootAsync():

// Definicja modułu dynamicznego
@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }

  static forRootAsync(options: AsyncDatabaseOptions): DynamicModule {
    return {
      module: DatabaseModule,
      imports: options.imports || [],
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject || [],
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

// Użycie
@Module({
  imports: [
    DatabaseModule.forRoot({
      host: 'localhost',
      port: 5432,
    }),
    // lub async
    DatabaseModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

Konwencje nazewnictwa:

  • forRoot() - konfiguracja globalna (raz w AppModule)
  • forFeature() - konfiguracja per-moduł
  • forRootAsync() - async konfiguracja z DI

Kontrolery i Routing

Czym jest kontroler w NestJS i za co odpowiada?

Odpowiedź w 30 sekund: Kontroler to klasa oznaczona @Controller(), która obsługuje przychodzące żądania HTTP i zwraca odpowiedzi. Odpowiada za routing - mapowanie URL i metod HTTP na odpowiednie handlery. Nie powinien zawierać logiki biznesowej - deleguje ją do serwisów.

Odpowiedź w 2 minuty: Kontroler jest warstwą odpowiedzialną za obsługę żądań HTTP i delegowanie logiki do serwisów. Poniżej pełny przykład kontrolera z podstawowymi operacjami CRUD:

@Controller('users') // Prefix: /users
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()                    // GET /users
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')               // GET /users/:id
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Post()                   // POST /users
  @HttpCode(201)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')               // PUT /users/:id
  update(
    @Param('id') id: string,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(+id, updateUserDto);
  }

  @Delete(':id')            // DELETE /users/:id
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id);
  }
}

Dekoratory parametrów:

  • @Param() - parametry ścieżki
  • @Query() - query parameters
  • @Body() - body żądania
  • @Headers() - nagłówki
  • @Req(), @Res() - obiekty request/response

Providery i Dependency Injection

Jak działa Dependency Injection w NestJS?

Odpowiedź w 30 sekund: NestJS ma wbudowany kontener IoC (Inversion of Control), który automatycznie zarządza zależnościami. Klasy oznaczone @Injectable() są rejestrowane jako providery i mogą być wstrzykiwane przez konstruktor. Kontener tworzy instancje i zarządza ich cyklem życia.

Odpowiedź w 2 minuty: System Dependency Injection w NestJS automatyzuje zarządzanie zależnościami między klasami. Poniższy przykład pokazuje kompletny przepływ od definicji serwisu po wstrzyknięcie:

// 1. Oznacz klasę jako injectable
@Injectable()
export class UsersService {
  constructor(
    private readonly usersRepository: UsersRepository,
    private readonly emailService: EmailService,
  ) {}

  async createUser(dto: CreateUserDto) {
    const user = await this.usersRepository.create(dto);
    await this.emailService.sendWelcome(user.email);
    return user;
  }
}

// 2. Zarejestruj w module
@Module({
  providers: [UsersService, UsersRepository, EmailService],
  controllers: [UsersController],
})
export class UsersModule {}

// 3. Wstrzyknij przez konstruktor
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

Typy providerów:

@Module({
  providers: [
    // Standard - klasa
    UsersService,

    // useClass - inna implementacja
    { provide: UsersService, useClass: MockUsersService },

    // useValue - stała wartość
    { provide: 'API_KEY', useValue: 'secret-key' },

    // useFactory - dynamiczne tworzenie
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        return createConnection(config.get('DATABASE_URL'));
      },
      inject: [ConfigService],
    },
  ],
})

Czym jest scope providera i jakie są dostępne opcje?

Odpowiedź w 30 sekund: Scope określa cykl życia providera. DEFAULT (singleton) - jedna instancja dla całej aplikacji. REQUEST - nowa instancja per żądanie HTTP. TRANSIENT - nowa instancja przy każdym wstrzyknięciu. Domyślnie wszystko jest singleton.

Odpowiedź w 2 minuty: NestJS oferuje trzy typy scope'ów dla providerów, każdy z innym cyklem życia. Oto przykłady poszczególnych wariantów:

// DEFAULT (singleton) - domyślny
@Injectable()
export class UsersService {}

// REQUEST scope - per żądanie
@Injectable({ scope: Scope.REQUEST })
export class RequestLoggerService {
  constructor(@Inject(REQUEST) private request: Request) {}
}

// TRANSIENT scope - nowa instancja zawsze
@Injectable({ scope: Scope.TRANSIENT })
export class HelperService {}

Kiedy używać:

Scope Użycie Wydajność
DEFAULT Stateless serwisy, większość przypadków Najlepsza
REQUEST Gdy potrzebujesz dostępu do request Średnia
TRANSIENT Gdy każdy consumer potrzebuje własnej instancji Najgorsza

Uwaga: REQUEST i TRANSIENT propagują się w górę drzewa zależności - jeśli serwis REQUEST jest wstrzykiwany do singleton, ten singleton też stanie się REQUEST-scoped.


Middleware, Guards, Interceptory, Pipes

Czym są Guards w NestJS i do czego służą?

Odpowiedź w 30 sekund: Guards to klasy implementujące CanActivate, które decydują czy żądanie powinno być obsłużone. Zwracają true (dostęp) lub false/wyjątek (brak dostępu). Używane głównie do autoryzacji i uwierzytelniania. Wykonywane po middleware, przed interceptorami.

Odpowiedź w 2 minuty: Guards implementują logikę kontroli dostępu w aplikacji. Poniżej przykłady dwóch najczęściej używanych guardów - do uwierzytelniania i autoryzacji:

// Auth Guard
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('Brak tokenu');
    }

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch {
      throw new UnauthorizedException('Nieprawidłowy token');
    }
  }
}

// Roles Guard z metadanymi
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>(
      'roles',
      context.getHandler(),
    );

    if (!requiredRoles) return true;

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some(role => user.roles?.includes(role));
  }
}

// Użycie
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
export class AdminController {
  @Get()
  @Roles('admin') // Custom decorator
  getAdminData() {
    return { secret: 'admin data' };
  }
}

Czym są Interceptory w NestJS i jakie mają zastosowania?

Odpowiedź w 30 sekund: Interceptory to klasy implementujące NestInterceptor, które mogą transformować request przed handlerem i response po handlerze. Używane do: logowania, cache'owania, transformacji odpowiedzi, timeout, obsługi błędów. Działają jak "opakowanie" wokół handlera.

Odpowiedź w 2 minuty: Interceptory wykorzystują RxJS do transformacji strumieni danych. Oto trzy praktyczne przykłady zastosowania interceptorów w aplikacji:

// Logging Interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const now = Date.now();

    console.log(`[${request.method}] ${request.url} - Start`);

    return next.handle().pipe(
      tap(() => {
        console.log(`[${request.method}] ${request.url} - ${Date.now() - now}ms`);
      }),
    );
  }
}

// Transform Response Interceptor
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

// Timeout Interceptor
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      timeout(5000),
      catchError(err => {
        if (err instanceof TimeoutError) {
          throw new RequestTimeoutException();
        }
        throw err;
      }),
    );
  }
}

Czym są Pipes w NestJS i do czego służą?

Odpowiedź w 30 sekund: Pipes to klasy implementujące PipeTransform, służące do transformacji i walidacji danych wejściowych. Wbudowane: ValidationPipe, ParseIntPipe, ParseBoolPipe. Wykonywane przed handlerem, mogą rzucać wyjątki jeśli dane są nieprawidłowe.

Odpowiedź w 2 minuty: Pipes w NestJS zapewniają walidację i transformację danych wejściowych przed ich przetworzeniem przez handlery. Poniżej przykłady użycia wbudowanych pipe'ów oraz tworzenia własnych:

// Wbudowane Pipes
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  // id jest już liczbą, nie stringiem
  return this.usersService.findOne(id);
}

// ValidationPipe z class-validator
// dto/create-user.dto.ts
export class CreateUserDto {
  @IsString()
  @MinLength(2)
  name: string;

  @IsEmail()
  email: string;

  @IsInt()
  @Min(18)
  age: number;
}

// Użycie
@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

// Globalny ValidationPipe (main.ts)
app.useGlobalPipes(new ValidationPipe({
  whitelist: true,        // Usuń nieznane właściwości
  forbidNonWhitelisted: true, // Błąd przy nieznanych
  transform: true,        // Auto-transformacja typów
}));

// Custom Pipe
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
  transform(value: string): Date {
    const date = new Date(value);
    if (isNaN(date.getTime())) {
      throw new BadRequestException('Invalid date format');
    }
    return date;
  }
}

Jaka jest kolejność wykonywania middleware, guards, interceptors i pipes?

Odpowiedź w 30 sekund: Kolejność: Middleware → Guards → Interceptors (before) → Pipes → Handler → Interceptors (after) → Exception Filters. Middleware nie ma dostępu do kontekstu wykonania, guards decydują o dostępie, pipes walidują/transformują dane.

Odpowiedź w 2 minuty: Zrozumienie kolejności wykonywania poszczególnych komponentów jest kluczowe dla debugowania i projektowania aplikacji. Poniższy diagram przedstawia pełny cykl przetwarzania żądania:

Request
   ↓
┌─────────────────┐
│   Middleware    │  → Klasyczne middleware, brak wiedzy o handlerze
└─────────────────┘
   ↓
┌─────────────────┐
│     Guards      │  → Autoryzacja, dostęp do ExecutionContext
└─────────────────┘
   ↓
┌─────────────────┐
│  Interceptors   │  → Pre-processing (przed handlerem)
│    (before)     │
└─────────────────┘
   ↓
┌─────────────────┐
│     Pipes       │  → Walidacja i transformacja parametrów
└─────────────────┘
   ↓
┌─────────────────┐
│    Handler      │  → Metoda kontrolera
└─────────────────┘
   ↓
┌─────────────────┐
│  Interceptors   │  → Post-processing (po handlerze)
│    (after)      │
└─────────────────┘
   ↓
┌─────────────────┐
│Exception Filters│  → Obsługa wyjątków (jeśli wystąpiły)
└─────────────────┘
   ↓
Response

Jeśli Guard zwróci false lub Pipe rzuci wyjątek, dalsze wykonanie jest przerywane i kontrola trafia do Exception Filters.


Obsługa błędów

Jak tworzyć własne Exception Filters w NestJS?

Odpowiedź w 30 sekund: Exception Filter to klasa z @Catch() implementująca ExceptionFilter. Przechwytuje wyjątki i formatuje odpowiedź błędu. Można łapać konkretne typy wyjątków lub wszystkie (@Catch()). Stosowane globalnie, per kontroler lub per handler.

Odpowiedź w 2 minuty: Exception Filters pozwalają na centralizację obsługi błędów i formatowanie odpowiedzi. Poniżej przykłady filtrów do obsługi różnych typów wyjątków:

// Custom Exception Filter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();
    const exceptionResponse = exception.getResponse();

    response.status(status).json({
      success: false,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof exceptionResponse === 'string'
        ? exceptionResponse
        : (exceptionResponse as any).message,
    });
  }
}

// Catch all exceptions
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    response.status(status).json({
      success: false,
      statusCode: status,
      message: 'Internal server error',
    });
  }
}

// Użycie
@UseFilters(HttpExceptionFilter)
@Controller('users')
export class UsersController {}

// Globalnie (main.ts)
app.useGlobalFilters(new AllExceptionsFilter());

Bazy danych

Jak zintegrować TypeORM z NestJS?

Odpowiedź w 30 sekund: Używając @nestjs/typeorm. Konfiguracja przez TypeOrmModule.forRoot() w AppModule. Encje definiuje się dekoratorami TypeORM. Repozytoria wstrzykuje się przez @InjectRepository(). Dostępny też wzorzec Repository z metodami CRUD.

Odpowiedź w 2 minuty: Integracja TypeORM z NestJS jest prosta dzięki dedykowanemu modułowi. Poniżej kompletny przykład konfiguracji, definicji encji i użycia w serwisie:

// app.module.ts
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'user',
      password: 'password',
      database: 'mydb',
      entities: [User],
      synchronize: true, // Tylko dev!
    }),
    UsersModule,
  ],
})
export class AppModule {}

// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @CreateDateColumn()
  createdAt: Date;
}

// users.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

// users.service.ts
@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: number): Promise<User> {
    return this.usersRepository.findOneBy({ id });
  }

  create(createUserDto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(createUserDto);
    return this.usersRepository.save(user);
  }
}

Testowanie

Jak testować jednostkowo serwisy w NestJS?

Odpowiedź w 30 sekund: Używając @nestjs/testing i Test.createTestingModule(). Tworzymy izolowany moduł z mockami zależności. Dla repozytoriów używamy getRepositoryToken(). Jest jako test runner. Mockujemy zewnętrzne zależności przez useValue lub useFactory.

Odpowiedź w 2 minuty: Testowanie w NestJS wykorzystuje moduł @nestjs/testing do tworzenia izolowanych kontekstów testowych. Poniżej kompletny przykład testowania serwisu z mockami zależności:

// users.service.spec.ts
describe('UsersService', () => {
  let service: UsersService;
  let repository: Repository<User>;

  const mockRepository = {
    find: jest.fn(),
    findOneBy: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(User),
          useValue: mockRepository,
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    repository = module.get<Repository<User>>(getRepositoryToken(User));
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('findAll', () => {
    it('should return array of users', async () => {
      const users = [{ id: 1, name: 'Jan', email: 'jan@example.com' }];
      mockRepository.find.mockResolvedValue(users);

      const result = await service.findAll();

      expect(result).toEqual(users);
      expect(mockRepository.find).toHaveBeenCalled();
    });
  });

  describe('create', () => {
    it('should create and return user', async () => {
      const dto = { name: 'Jan', email: 'jan@example.com' };
      const user = { id: 1, ...dto };

      mockRepository.create.mockReturnValue(user);
      mockRepository.save.mockResolvedValue(user);

      const result = await service.create(dto);

      expect(result).toEqual(user);
      expect(mockRepository.create).toHaveBeenCalledWith(dto);
      expect(mockRepository.save).toHaveBeenCalledWith(user);
    });
  });
});

Zobacz też


Ten artykuł jest częścią serii przygotowującej do rozmów rekrutacyjnych na stanowisko Node.js Backend Developer. Sprawdź nasze fiszki NestJS z 42 pytaniami i odpowiedziami do nauki.

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.