NestJS: Eksiksiz bir REST API Oluşturma

NestJS ile profesyonel bir REST API oluşturmak için kapsamlı rehber. Controller, Service, Module yapıları, class-validator ile doğrulama ve hata yönetimi pratik örneklerle açıklanmaktadır.

NestJS ile eksiksiz bir REST API oluşturma rehberi

NestJS, ölçeklenebilir ve bakımı kolay sunucu tarafı uygulamalar geliştirmek için Node.js ekosisteminin öncü framework'ü konumundadır. Angular'dan ilham alan modüler mimarisi, bağımlılık enjeksiyonu ve yerel TypeScript desteği sunar. Profesyonel REST API geliştirme süreci bu yapı sayesinde düzenli ve öngörülebilir hale gelir.

2026'da Neden NestJS

NestJS 11, önemli performans iyileştirmeleri, graceful shutdown için yerel sinyal desteği ve Prisma ile Drizzle gibi modern ORM'lerle basitleştirilmiş entegrasyon sunmaktadır. Framework, önceki sürümlerle tam geriye dönük uyumluluğunu korumaktadır.

Proje Kurulumu ve Yapılandırma

NestJS CLI, üretime hazır bir yapıyla eksiksiz bir proje oluşturur. TypeScript, ESLint ve birim testleri otomatik olarak yapılandırılır.

bash
# terminal
# Global installation of the NestJS CLI
npm install -g @nestjs/cli

# Create a new project
nest new my-api

# Navigate to the project
cd my-api

# Start in development mode with hot-reload
npm run start:dev

Bu komut, düzenli bir dosya yapısı ve önceden yüklenmiş temel bağımlılıklarla bir proje oluşturur.

src/main.tstypescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  // Create the NestJS application
  const app = await NestFactory.create(AppModule);

  // Enable global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,           // Removes non-decorated properties
    forbidNonWhitelisted: true, // Rejects requests with unknown properties
    transform: true,            // Transforms payloads to DTO instances
  }));

  // Configure global prefix for all routes
  app.setGlobalPrefix('api');

  await app.listen(3000);
}
bootstrap();

Bu yapılandırma, otomatik istek doğrulamayı etkinleştirir ve tüm rotalara /api önekini ekler.

Modüler Mimariyi Anlamak

NestJS, kodu modüller halinde organize eder; her modül bir işlevsel alanı kapsar. Modüller, controller'lar (HTTP endpoint'leri), provider'lar (iş servisleri) ve import'lar (bağımlılıklar) tanımlar.

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';
import { AuthModule } from './auth/auth.module';

// The root module imports all application modules
@Module({
  imports: [
    UsersModule,    // User management
    ProductsModule, // Product catalog
    AuthModule,     // Authentication
  ],
})
export class AppModule {}

Her iş modülü aynı yapıyı takip eder: bir modül dosyası, bir controller ve bir service.

src/users/users.module.tstypescript
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  // Controllers handle HTTP requests
  controllers: [UsersController],
  // Providers are injectable throughout the module
  providers: [UsersService],
  // Exports make providers available to other modules
  exports: [UsersService],
})
export class UsersModule {}

UsersService'in dışa aktarılması, diğer modüllerin UsersModule'ü import ederek bu servisi bağımlılık enjeksiyonu yoluyla kullanmasına olanak tanır.

Eksiksiz Bir CRUD Controller Oluşturma

Controller'lar HTTP rotalarını tanımlar ve iş mantığını servislere devreder. NestJS dekoratörleri kodu ifade gücü yüksek ve kendi kendini belgeleyen bir yapıya kavuşturur.

src/users/users.controller.tstypescript
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  Query,
  HttpCode,
  HttpStatus,
  ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

// Route prefix: /api/users
@Controller('users')
export class UsersController {
  // Inject service via constructor
  constructor(private readonly usersService: UsersService) {}

  // POST /api/users - Create a user
  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() createUserDto: CreateUserDto): Promise<User> {
    // The DTO is automatically validated before reaching here
    return this.usersService.create(createUserDto);
  }

  // GET /api/users - List with pagination
  @Get()
  async findAll(
    @Query('page', new ParseIntPipe({ optional: true })) page: number = 1,
    @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10,
  ): Promise<{ data: User[]; total: number }> {
    return this.usersService.findAll(page, limit);
  }

  // GET /api/users/:id - Retrieve by ID
  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
    // ParseIntPipe automatically converts and validates the parameter
    return this.usersService.findOne(id);
  }

  // PUT /api/users/:id - Full update
  @Put(':id')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ): Promise<User> {
    return this.usersService.update(id, updateUserDto);
  }

  // DELETE /api/users/:id - Deletion
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
    await this.usersService.remove(id);
  }
}

ParseIntPipe gibi pipe'lar parametre dönüşümü ve doğrulamasını sağlar. Başarısızlık durumunda otomatik olarak 400 hatası döndürülür.

Yerleşik Doğrulama Pipe'ları

NestJS birkaç yerleşik pipe sunar: ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe. Her biri, handler çalıştırılmadan önce gelen veriyi doğrular ve dönüştürür.

İş Servisinin Uygulanması

Servisler, iş mantığını ve veri erişimini kapsüller. @Injectable() dekoratörü ile işaretlenen servisler, NestJS bağımlılık enjeksiyon konteyneri tarafından yönetilir.

src/users/users.service.tstypescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';

@Injectable()
export class UsersService {
  // In-memory database simulation
  private users: User[] = [];
  private idCounter = 1;

  async create(createUserDto: CreateUserDto): Promise<User> {
    // Create the user entity
    const user: User = {
      id: this.idCounter++,
      ...createUserDto,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    this.users.push(user);
    return user;
  }

  async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
    // Calculate pagination
    const start = (page - 1) * limit;
    const end = start + limit;

    return {
      data: this.users.slice(start, end),
      total: this.users.length,
    };
  }

  async findOne(id: number): Promise<User> {
    const user = this.users.find(u => u.id === id);

    // Throw exception if user doesn't exist
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
    const user = await this.findOne(id);

    // Merge existing data with updates
    Object.assign(user, updateUserDto, { updatedAt: new Date() });

    return user;
  }

  async remove(id: number): Promise<void> {
    const index = this.users.findIndex(u => u.id === id);

    if (index === -1) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    this.users.splice(index, 1);
  }

  // Utility method for other services
  async findByEmail(email: string): Promise<User | undefined> {
    return this.users.find(u => u.email === email);
  }
}

NotFoundException istisnası, biçimlendirilmiş bir hata mesajıyla otomatik olarak HTTP 404 yanıtı oluşturur.

Node.js / NestJS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

class-validator ile Veri Doğrulama

DTO'lar (Data Transfer Objects) gelen verinin yapısını tanımlar. class-validator kütüphanesindeki dekoratörler, otomatik olarak uygulanan doğrulama kurallarını belirler.

src/users/dto/create-user.dto.tstypescript
import {
  IsEmail,
  IsNotEmpty,
  IsString,
  MinLength,
  MaxLength,
  IsOptional,
  Matches,
} from 'class-validator';

export class CreateUserDto {
  @IsNotEmpty({ message: 'Email is required' })
  @IsEmail({}, { message: 'Invalid email format' })
  email: string;

  @IsNotEmpty({ message: 'Password is required' })
  @MinLength(8, { message: 'Password must be at least 8 characters' })
  @MaxLength(50, { message: 'Password cannot exceed 50 characters' })
  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
    { message: 'Password must contain at least one uppercase, one lowercase, and one number' }
  )
  password: string;

  @IsNotEmpty({ message: 'First name is required' })
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  firstName: string;

  @IsNotEmpty({ message: 'Last name is required' })
  @IsString()
  @MinLength(2)
  @MaxLength(50)
  lastName: string;

  @IsOptional()
  @IsString()
  @MaxLength(20)
  phone?: string;
}

Kısmi güncellemeler için PartialType, doğrulama kurallarını koruyarak tüm alanları opsiyonel hale getirir.

src/users/dto/update-user.dto.tstypescript
import { PartialType, OmitType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

// All fields from CreateUserDto become optional
// Password is excluded from standard updates
export class UpdateUserDto extends PartialType(
  OmitType(CreateUserDto, ['password'] as const)
) {}

Entity, saklanan verinin yapısını temsil eder.

src/users/entities/user.entity.tstypescript
export class User {
  id: number;
  email: string;
  password: string;
  firstName: string;
  lastName: string;
  phone?: string;
  createdAt: Date;
  updatedAt: Date;
}

Merkezi Hata Yönetimi

NestJS, yerleşik HTTP istisnaları sunar. Global bir istisna filtresi, hata yanıt formatlarının özelleştirilmesine olanak tanır.

src/common/filters/http-exception.filter.tstypescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

// Catches all HttpExceptions
@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();

    // Retrieve error message (can be string or object)
    const exceptionResponse = exception.getResponse();
    const message = typeof exceptionResponse === 'string'
      ? exceptionResponse
      : (exceptionResponse as any).message;

    // Standardized response format
    response.status(status).json({
      success: false,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      message: message,
    });
  }
}

HTTP dışı hataları (sistem hataları, yakalanmamış istisnalar) yakalamak için ikinci bir filtre tam kapsam sağlar.

src/common/filters/all-exceptions.filter.tstypescript
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

// Catches ALL exceptions (including system errors)
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    // Determine HTTP code and message
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const message = exception instanceof HttpException
      ? exception.message
      : 'Internal server error';

    // Log error for debugging
    console.error('Exception caught:', exception);

    response.status(status).json({
      success: false,
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message,
    });
  }
}

Ana modülde global filtre kaydı.

src/main.ts (updated)typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
  }));

  // Global exception filters
  app.useGlobalFilters(
    new AllExceptionsFilter(),
    new HttpExceptionFilter(),
  );

  app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();
Filtre Sırası Önemlidir

Filtre kayıt sırası önemlidir. İlk kaydedilen filtre en son çalıştırılır. AllExceptionsFilter, yedek mekanizma olarak görev yapabilmesi için HttpExceptionFilter'dan önce kaydedilmelidir.

Prisma ORM Entegrasyonu

Prisma, otomatik olarak oluşturulan tipli bir istemci aracılığıyla veritabanı etkileşimlerini basitleştirir. NestJS ile eksiksiz entegrasyonu aşağıda sunulmaktadır.

bash
# terminal
# Install Prisma
npm install prisma @prisma/client

# Initialize Prisma with PostgreSQL
npx prisma init --datasource-provider postgresql

Veri şeması tanımı.

prisma/schema.prismaprisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  firstName String   @map("first_name")
  lastName  String   @map("last_name")
  phone     String?
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // Relations
  posts     Post[]

  @@map("users")
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  authorId  Int      @map("author_id")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // Relation to User
  author    User     @relation(fields: [authorId], references: [id])

  @@map("posts")
}

Yeniden kullanılabilir bir Prisma modülü oluşturma.

src/prisma/prisma.service.tstypescript
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  // Automatic connection on module startup
  async onModuleInit() {
    await this.$connect();
  }

  // Clean disconnection on application shutdown
  async onModuleDestroy() {
    await this.$disconnect();
  }
}
src/prisma/prisma.module.tstypescript
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

// @Global makes the service available throughout the application
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

Prisma kullanan güncellenmiş Users servisi.

src/users/users.service.ts (Prisma version)typescript
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from '@prisma/client';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  async create(createUserDto: CreateUserDto): Promise<Omit<User, 'password'>> {
    // Check email uniqueness
    const existingUser = await this.prisma.user.findUnique({
      where: { email: createUserDto.email },
    });

    if (existingUser) {
      throw new ConflictException('This email is already in use');
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);

    // Create the user
    const user = await this.prisma.user.create({
      data: {
        ...createUserDto,
        password: hashedPassword,
      },
    });

    // Exclude password from response
    const { password, ...result } = user;
    return result;
  }

  async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> {
    // Parallel execution of count and paginated query
    const [data, total] = await Promise.all([
      this.prisma.user.findMany({
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { createdAt: 'desc' },
        select: {
          id: true,
          email: true,
          firstName: true,
          lastName: true,
          phone: true,
          createdAt: true,
          updatedAt: true,
        },
      }),
      this.prisma.user.count(),
    ]);

    return { data: data as User[], total };
  }

  async findOne(id: number): Promise<Omit<User, 'password'>> {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: {
        id: true,
        email: true,
        firstName: true,
        lastName: true,
        phone: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user as Omit<User, 'password'>;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
    // Check existence
    await this.findOne(id);

    const user = await this.prisma.user.update({
      where: { id },
      data: updateUserDto,
      select: {
        id: true,
        email: true,
        firstName: true,
        lastName: true,
        phone: true,
        createdAt: true,
        updatedAt: true,
      },
    });

    return user as Omit<User, 'password'>;
  }

  async remove(id: number): Promise<void> {
    await this.findOne(id);
    await this.prisma.user.delete({ where: { id } });
  }
}

Yanıt Dönüşümü için Interceptor'lar

Interceptor'lar, yanıtların tekdüze bir şekilde dönüştürülmesini sağlar. Bir dönüşüm interceptor'ı, tüm API yanıtlarının formatını standartlaştırır.

src/common/interceptors/transform.interceptor.tstypescript
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

// Interface for standardized response format
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  timestamp: string;
}

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

Bir loglama interceptor'ı, istekleri ve yürütme sürelerini izler.

src/common/interceptors/logging.interceptor.tstypescript
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const now = Date.now();

    return next.handle().pipe(
      tap(() => {
        const response = context.switchToHttp().getResponse();
        const { statusCode } = response;
        const duration = Date.now() - now;

        // Structured log format
        this.logger.log(
          `${method} ${url} ${statusCode} - ${duration}ms`
        );
      }),
    );
  }
}

Sonuc

NestJS, profesyonel REST API geliştirmek için sağlam ve ölçeklenebilir bir mimari sunar. TypeScript, bağımlılık enjeksiyonu ve ifade gücü yüksek dekoratörlerin birleşimi, bakımı kolay ve test edilebilir uygulamalar oluşturmayı mümkün kılar.

Kaliteli Bir NestJS API icin Kontrol Listesi

  • Sorumluluk ayrımı ile modüler yapı
  • Tüm girdiler için class-validator doğrulamalı DTO'lar
  • İş mantığı için ayrılmış servisler
  • İstisna filtreleri ile merkezi hata yönetimi
  • Dönüşüm ve loglama için interceptor'lar
  • Veritabanı erişimi için Prisma entegrasyonu
  • Whitelist etkin global ValidationPipe
  • Tüm rotalarda tutarlı API öneki

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

NestJS'in gücü, doğal olarak en iyi uygulamalara yönlendiren katı yapısında yatar. Bağımlılık enjeksiyonu ve katmanlı ayrım gibi kanıtlanmış desenler, test edilebilir ve geliştirilebilir kod üretir; kurumsal uygulamalar için hazır bir temel oluşturur.

Etiketler

#nestjs
#nodejs
#typescript
#rest api
#backend

Paylaş

İlgili makaleler