Logo

dev-resources.site

for different kinds of informations.

Building Type-Safe APIs: Integrating NestJS with Prisma and TypeScript

Published at
1/7/2025
Categories
nextjs
backend
typescript
prisma
Author
jordan wilfry
Categories
4 categories in total
nextjs
open
backend
open
typescript
open
prisma
open
Building Type-Safe APIs: Integrating NestJS with Prisma and TypeScript

As a full-stack developer, type safety has become increasingly crucial in building robust and maintainable applications. In this comprehensive guide, we'll explore how to create a fully type-safe API using NestJS, Prisma, and TypeScript. By the end, you'll have a solid understanding of how to build APIs that catch errors at compile-time rather than runtime.

🎯 What We'll Cover

  1. Setting up a NestJS project with TypeScript
  2. Integrating Prisma with NestJS
  3. Creating type-safe database models
  4. Implementing CRUD operations with full type safety
  5. Best practices for error handling
  6. Advanced TypeScript features for better type safety

Prerequisites

  • Node.js (v18 or later)
  • Basic understanding of TypeScript
  • Familiarity with RESTful APIs
  • PostgreSQL installed locally

Initial Setup

First, let's create a new NestJS project with TypeScript:

npm i -g @nestjs/cli
nest new type-safe-api
cd type-safe-api

Now, let's add Prisma to our project:

npm install @prisma/client
npm install prisma --save-dev
npx prisma init

Defining Our Schema

Let's create a simple blog API with posts and categories. Here's our Prisma schema:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

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

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  category  Category @relation(fields: [categoryId], references: [id])
  categoryId Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Category {
  id    Int     @id @default(autoincrement())
  name  String  @unique
  posts Post[]
}

Creating Type-Safe DTOs

One of the key benefits of using NestJS with TypeScript is the ability to create type-safe DTOs:

// src/posts/dto/create-post.dto.ts

import { IsString, IsBoolean, IsNumber, IsOptional } from 'class-validator';

export class CreatePostDto {
  @IsString()
  title: string;

  @IsString()
  content: string;

  @IsBoolean()
  @IsOptional()
  published?: boolean;

  @IsNumber()
  categoryId: number;
}

Implementing the Posts Service

Here's how we can implement a type-safe service layer:

// src/posts/posts.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Post, Prisma } from '@prisma/client';
import { CreatePostDto } from './dto/create-post.dto';

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

  async create(data: CreatePostDto): Promise<Post> {
    return this.prisma.post.create({
      data: {
        ...data,
        published: data.published ?? false,
      },
      include: {
        category: true,
      },
    });
  }

  async findOne(id: number): Promise<Post> {
    const post = await this.prisma.post.findUnique({
      where: { id },
      include: {
        category: true,
      },
    });

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

    return post;
  }
}

Error Handling with Type Safety

Let's implement a custom error handling mechanism that leverages TypeScript's type system:

// src/common/errors/api-error.ts

export class ApiError extends Error {
  constructor(
    public readonly code: string,
    public readonly message: string,
    public readonly status: number = 400,
    public readonly details?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'ApiError';
  }

  static notFound(resource: string, id: number | string): ApiError {
    return new ApiError(
      'NOT_FOUND',
      `${resource} with ID ${id} not found`,
      404
    );
  }
}

Implementing Controllers with Type Safety

Here's how we can implement a type-safe controller:

// src/posts/posts.controller.ts

import { Controller, Get, Post, Body, Param, ParseIntPipe } from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findOne(id);
  }
}

Advanced Type Safety Features

1. Custom Type Guards

// src/common/guards/type-guards.ts

export function isPost(obj: unknown): obj is Post {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'title' in obj &&
    'content' in obj
  );
}

2. Generic Response Types

// src/common/types/api-response.type.ts

export interface ApiResponse<T> {
  data: T;
  meta?: {
    count?: number;
    page?: number;
    totalPages?: number;
  };
}

Testing Our Type-Safe API

Here's an example of how to write tests that leverage our type system:

// src/posts/posts.service.spec.ts

import { Test } from '@nestjs/testing';
import { PostsService } from './posts.service';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePostDto } from './dto/create-post.dto';

describe('PostsService', () => {
  let service: PostsService;
  let prisma: PrismaService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [PostsService, PrismaService],
    }).compile();

    service = module.get<PostsService>(PostsService);
    prisma = module.get<PrismaService>(PrismaService);
  });

  it('should create a post', async () => {
    const dto: CreatePostDto = {
      title: 'Test Post',
      content: 'Test Content',
      categoryId: 1,
    };

    const created = await service.create(dto);
    expect(created.title).toBe(dto.title);
  });
});

Best Practices and Tips

  1. Always Use Strict TypeScript Configuration
   {
     "compilerOptions": {
       "strict": true,
       "strictNullChecks": true,
       "strictPropertyInitialization": true
     }
   }
  1. Leverage Prisma's Generated Types

    • Use Prisma's generated types instead of creating your own interface definitions
    • Take advantage of Prisma's type-safe query builders
  2. Implement Custom Decorators for Common Validations

   export function IsNonEmptyString() {
     return applyDecorators(
       IsString(),
       IsNotEmpty(),
       Transform(({ value }) => value?.trim())
     );
   }
  1. Use Enums for Type-Safe Constants
   export enum PostStatus {
     DRAFT = 'DRAFT',
     PUBLISHED = 'PUBLISHED',
     ARCHIVED = 'ARCHIVED'
   }

Conclusion

Building type-safe APIs with NestJS, Prisma, and TypeScript provides several benefits:

  • Catch errors at compile-time rather than runtime
  • Improved developer experience with better IDE support
  • Self-documenting code through type definitions
  • Reduced need for runtime validation
  • Better maintainability and refactoring capabilities

The combination of these tools creates a robust development environment where you can build APIs with confidence, knowing that many potential issues will be caught before they reach production.

Additional Resources

How are you using type safety in your NestJS applications? Share your experiences and best practices in the comments below!

Follow me for more articles on full-stack development, TypeScript, and software architecture.

🚀 Happy coding! 🚀

Featured ones: