DEV Community

Cover image for Building Type-Safe APIs: Integrating NestJS with Prisma and TypeScript
jordan wilfry
jordan wilfry

Posted on

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
Enter fullscreen mode Exit fullscreen mode

Now, let's add Prisma to our project:

npm install @prisma/client
npm install prisma --save-dev
npx prisma init
Enter fullscreen mode Exit fullscreen mode

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[]
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  );
}
Enter fullscreen mode Exit fullscreen mode

2. Generic Response Types

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

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

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);
  });
});
Enter fullscreen mode Exit fullscreen mode

Best Practices and Tips

  1. Always Use Strict TypeScript Configuration
   {
     "compilerOptions": {
       "strict": true,
       "strictNullChecks": true,
       "strictPropertyInitialization": true
     }
   }
Enter fullscreen mode Exit fullscreen mode
  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())
     );
   }
Enter fullscreen mode Exit fullscreen mode
  1. Use Enums for Type-Safe Constants
   export enum PostStatus {
     DRAFT = 'DRAFT',
     PUBLISHED = 'PUBLISHED',
     ARCHIVED = 'ARCHIVED'
   }
Enter fullscreen mode Exit fullscreen mode

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! ๐Ÿš€

Top comments (0)