Let's Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD

  • nestjs
    nestjs
  • prisma
    prisma
  • postgresql
    postgresql
  • docker
    docker
Published on 2025/04/12

Introduction

In this article, we will walk through how to implement an article posting API using NestJS and Prisma, explaining each step carefully from the basics.

NestJS is attracting attention as a framework with excellent extensibility and maintainability, and it is a technology that can be safely adopted even for medium- to large-scale applications. Prisma, on the other hand, is a type-safe and intuitive ORM that works extremely well with TypeScript and has been adopted in many projects in recent years.

Intended readers

This article is aimed at readers who already have a basic understanding of NestJS’s structure. In particular:

  • Those who know the basic project structure of NestJS but are not yet familiar with how to build practical APIs
  • Those who want to try Prisma and experience how to apply it to real API development
  • Those who want to learn an API structure that is “useful in real-world projects,” not just for simple behavior checks

https://shinagawa-web.com/en/blogs/nestjs-blog-series-setup-and-config

Goal of this article

In this article, we aim to achieve the following:

  • Introduce Prisma and be able to define and migrate an article (Post) model
  • Build CRUD APIs (list, create, detail, update, delete) for article posting on NestJS
  • Implement with a robust structure by incorporating validation and DTO design

By understanding this structure, you should improve your “on-site adaptability” so that your code can withstand real product development.

We will implement up to the point where you can actually create and retrieve articles via API tests using Postman.

Image from Gyazo

The code implemented in this article is stored in the following repository, so please refer to it together.

https://github.com/shinagawa-web/nestjs-blog

Overview of this series and where this article fits

Series structure

  1. Start Web App Development with NestJS ─ Learn Project Structure and Configuration Management by Building a Blog Site
  2. Let's Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD ← This article
  3. Enhancing Application Reliability ─ Logging, Error Handling, and Test Strategy
  4. Deep Dive into Prisma and DB Design ─ Models, Relations, and Operational Design
  5. Build the UI with React and Deploy to Production ─ Docker and Environment Setup

https://shinagawa-web.com/en/blogs/nestjs-blog-series-setup-and-config

https://shinagawa-web.com/en/blogs/nestjs-blog-series-logging-error-testing

https://shinagawa-web.com/en/blogs/nestjs-blog-series-prisma-db-design

https://shinagawa-web.com/en/blogs/nestjs-blog-series-react-deploy

Launching PostgreSQL with Docker

First, we will start PostgreSQL on Docker so that NestJS + Prisma can connect to it.

Create docker-compose.yml in the project root

docker-compose.yml
services:
  db:
    image: postgres:15
    container_name: nestjs-postgres
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: nestjs_blog
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
  • If you persist data with a volume, the data will not be lost even if the container is restarted.

Startup command

This will start the container in the background.

docker-compose up -d

Check DB connection

docker exec -it nestjs-postgres psql -U postgres -d nestjs_blog

Once inside the container, check the list of databases.

\l

If nestjs_blog is displayed, you are good to go.

Image from Gyazo

There is no particular need to work directly inside the container, so exit for now.

\q

Add connection information to .env

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/nestjs_blog"

With this connection string, Prisma Client will be able to access PostgreSQL running on Docker.

Introducing Prisma and Initial Setup

As the ORM (Object-Relational Mapping) that forms the foundation of our article posting API, we will use Prisma. Prisma is a modern ORM tightly integrated with TypeScript, and its major feature is that it allows type-safe and intuitive DB operations.

First, let’s introduce Prisma into the NestJS project and set up the connection to the database.

https://www.prisma.io/

Installing Prisma

First, install the required packages. Run the following commands at the root of your NestJS project.

cd backend
npm install prisma --save-dev
npm install @prisma/client
  • prisma is the CLI and migration tool
  • @prisma/client is the library used by the application to interact with the DB

Run prisma init

Next, run the Prisma initialization command.

npx prisma init
  • prisma/schema.prisma: The model definition file will be created.
prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  output   = "../generated/prisma"
}

If it is configured like this, the output destination of the Prisma Client generated later will be /generate, and NestJS will not be able to recognize it correctly, so delete this setting.

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

Model Definition: Creating the Article (Post) Model

Now that Prisma has been introduced and the connection settings are complete, the next step is to define a model for article posting.

Here, we will create a model named Post and design the fields required for article posting. We assume that an article will include a title, body, creation date, update date, and so on.

Edit schema.prisma

Edit backend/prisma/schema.prisma as follows.

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

With this definition, the Post model will include the following fields:

Field name Type Notes
id Int Primary key. Auto-incrementing integer
title String Article title
content String Article body
createdAt DateTime Creation timestamp. Automatically set to the current time
updatedAt DateTime Update timestamp. Automatically updated when the record is updated

In Prisma, you can define the behavior of fields such as default values and automatic updates using annotations like @default and @updatedAt.

Running the migration

After defining the model, run a migration to create the table in the database.

cd backend
npx prisma migrate dev --name init

Image from Gyazo

When you run this command, the following will happen:

  • A migration history will be generated in the migrations/ directory
  • A Post table will be created in PostgreSQL based on the Prisma model definition
  • @prisma/client will be automatically regenerated, enabling type-safe DB access

Confirming table creation (using Prisma Studio)

Now that the migration is complete, let’s use Prisma Studio to check the table creation status.

Prisma Studio is a web-based data browser provided by Prisma, which allows you to conveniently inspect the contents of your local DB via a GUI.

Start it with the following command:

npx prisma studio

Image from Gyazo

When you run the command, your browser will open, and you can confirm that the Post table is displayed.
The table name appears in the left sidebar, and by clicking it you can view the contents and add or edit records.

Since you can check things via a GUI, it’s easy to intuitively confirm whether the migration has been applied correctly.

Of course, you can also check via the CLI, but for efficiency and visibility, we will basically use Prisma Studio for confirmation from now on.

Next, we will implement APIs (CRUD operations) to actually read and write data using this Post model. With the model and table in place, the backbone of the backend is now visible.

Generating Prisma Client and Integrating It into NestJS

We have defined the article model and created the table in the database via migration, so the data structure is now ready.
The next step is to prepare a mechanism to access that database.

What is Prisma Client?

In Prisma, Prisma Client is automatically generated from the model definitions written in schema.prisma. This is the code used to access the database.

prisma.post.findMany() // Retrieve all records from the Post table
prisma.post.create({ data: { title, content } }) // Create a new record

In this way, it becomes a client library that allows type-safe DB operations.

This Prisma Client is generated inside the @prisma/client package and can be used from NestJS as well.

Generating Prisma Client

You can generate it with the following command:

cd backend
npx prisma generate

The generated files are included in node_modules/@prisma/client, where Prisma’s type information is also defined.

How to use Prisma in NestJS?

In NestJS, instead of directly calling new PrismaClient(), it is common to use NestJS’s dependency injection (DI) mechanism to share it across the application.

To do this, we create a PrismaService that wraps Prisma Client.

What is PrismaService?

PrismaService is a class that plays the following roles:

  • Extends PrismaClient so it can be reused across the entire app
  • Registers as a NestJS “service” so it can be used in Controllers and other Services
  • Connects to the DB when the app starts and disconnects when it shuts down
backend/src/prisma/prisma.service.ts
import { PrismaClient } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import { OnModuleDestroy, OnModuleInit } from '@nestjs/common/interfaces/hooks';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect(); // Connect to DB when the app starts
  }

  async onModuleDestroy() {
    await this.$disconnect(); // Disconnect when the app shuts down
  }
}

This allows the entire application to share a single PrismaService instance.

We then create a module that provides PrismaService and import it where needed so that Prisma’s features can be used wherever required. This is in line with NestJS’s design philosophy.

backend/src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService], // Make it available to other modules
})
export class PrismaModule {}

By importing this module with imports: [PrismaModule] in the PostModule and AppModule that we will create later, PrismaService becomes available anywhere via NestJS’s dependency injection.

Summary so far

Concept Role
Prisma Client Type-safe DB operation library generated by Prisma
PrismaService Service that centrally manages Prisma Client in NestJS (connection, disconnection, injection management)
PrismaModule NestJS module that provides and shares PrismaService

Creating CRUD APIs (Implementing Controller / Service)

Now that the Post model has been defined and the table created, we will implement CRUD functionality for article posting.
Here, we will use NestJS Controllers and Services to build the following five endpoints:

  • Get article list (GET /posts)
  • Create a new article (POST /posts)
  • Get article details (GET /posts/:id)
  • Update an article (PATCH /posts/:id)
  • Delete an article (DELETE /posts/:id)

We will use Prisma Client to perform all of these operations in a type-safe manner.

Creating PostModule

First, generate the module, controller, and service for articles all at once.

cd backend
npx nest g resource posts

Image from Gyazo

This will automatically generate the necessary files under src/posts/.

  • posts.controller.ts
  • posts.service.ts
  • posts.module.ts
  • dto/create-post.dto.ts
  • dto/update-post.dto.ts

At this stage, a basic skeleton for CRUD APIs has been set up.

Aligning DTO types with the model

The DTO needs to correspond to PostCreateInput, so modify it as follows:

backend/src/posts/dto/create-post.dto.ts
export class CreatePostDto {
+ title: string;
+ content: string;
}

We will explain DTOs in more detail later.

Injecting PrismaService

In PostService, we use PrismaService for database operations.
In NestJS, to inject service dependencies, we write the constructor of PostService as follows:

backend/src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { PrismaService } from 'src/prisma/prisma.service';

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

  create(createPostDto: CreatePostDto) {
    return this.prisma.post.create({ data: createPostDto });
  }

  findAll() {
    return this.prisma.post.findMany();
  }

  findOne(id: number) {
    return this.prisma.post.findUnique({ where: { id } });
  }

  update(id: number, updatePostDto: UpdatePostDto) {
    return this.prisma.post.update({
      where: { id },
      data: updatePostDto,
    });
  }

  remove(id: number) {
    return this.prisma.post.delete({ where: { id } });
  }
}

Since there are many changes, we have shown the final code.

In NestJS, to inject a service, the module must “provide” that service.
In this case, we chose a structure where PrismaService is provided and exported by PrismaModule.

backend/src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { PostService } from './post.service';
import { PostController } from './post.controller';
+ import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
+ imports: [PrismaModule],
  controllers: [PostController],

API Testing with Postman

After finishing the code changes, start NestJS.

cd backend
npm run start:dev

This time, we will use PostMan to access the API and check whether we can register and retrieve posts.

https://www.postman.com/

There is a Postman VSCode extension, so I use that to perform behavior checks.

https://marketplace.visualstudio.com/items?itemName=Postman.postman-for-vscode

① Create a new article (POST /posts)

{
  "title": "My first article",
  "content": "This is the first post created with a NestJS + Prisma API."
}

If the request succeeds, you will receive a 201 Created status and the JSON of the created article.

Image from Gyazo

② Get article list (GET /posts)

The articles you created will be returned as an array.

Image from Gyazo

③ Get article details (GET /posts/:id)

You can retrieve a single article corresponding to the specified ID.

Image from Gyazo

④ Update an article (PATCH /posts/:id)

{
  "title": "The article title has been updated"
}

Partial updates are also possible. The response will contain the updated data.

Image from Gyazo

⑤ Delete an article (DELETE /posts/:id)

If successful, you will receive a 200 OK status and the information of the deleted article.

Image from Gyazo

After that, if you run the article list retrieval again, you can confirm that no records are returned.

Image from Gyazo

Validation and DTO Design

To ensure that only valid data is sent to the article posting API, we will design DTOs and validation.
In NestJS, you can concisely implement validation logic by adding class-validator decorators to DTOs.

What is a DTO?

A DTO (Data Transfer Object) is a type definition that explicitly describes the structure of requests and responses.
In NestJS, defining DTOs as classes not only enables TypeScript type checking but also makes them targets for validation.

For example, the data structure required when creating an article can be designed as follows.

Install packages

npm install class-validator class-transformer

Add validation to CreatePostDto

Edit the CLI-generated create-post.dto.ts as follows:

backend/src/posts/dto/create-post.dto.ts
+ import { IsString, IsNotEmpty } from 'class-validator';

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

+ @IsString()
+ @IsNotEmpty()
  content: string;
}

This requires that the JSON sent to the API has the following format and that the fields are not empty:

{
  "title": "Article Posting API with NestJS and Prisma",
  "content": "By applying validation, you can ensure a reliable data structure."
}

UpdatePostDto

To support partial updates, properties in UpdatePostDto are made optional.
You can leave the CLI-generated code as is.

backend/src/posts/dto/update-post.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreatePostDto } from './create-post.dto';

export class UpdatePostDto extends PartialType(CreatePostDto) {}

Enabling validation

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
+ app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap().catch((err) => {
  console.error('Application failed to start', err);
});

Behavior check

Confirm that NestJS automatically detects validation errors and returns an error response.

Create a new article (POST /posts)

{}

If you send empty data to POST /posts, you will receive a response like the following:

Image from Gyazo

Conclusion

In this article, we built a simple article posting API from scratch using NestJS and Prisma.
After introducing Prisma and confirming the connection with PostgreSQL, we proceeded step by step through model definition, migration, Prisma Client integration, REST API implementation, and validation design using DTOs.

With the structure we have built so far, we now have:

  • Type-safe and highly maintainable data operations
  • Endpoint design that follows REST principles
  • A validation mechanism that prevents invalid requests

In other words, we have completed a solid and practical foundation for a backend API.

With this foundation in place, you should be able to smoothly incorporate more advanced features such as:

  • Implementing authentication and authorization to associate logged-in users with articles
  • Designing relations with other entities such as users, comments, and tags
  • Performing advanced DB operations using Prisma middleware and transactions
  • Automatically generating API documentation with Swagger

Backend development based on NestJS and Prisma offers an excellent balance of type safety, flexibility, and readability, making it well-suited for team development.
I hope this article helps you update and improve your own development style.

Next time

So far, we have implemented all the basic functions of an article posting API. Next time, we will go a step further and explore techniques to turn this into a more “practical application.”

  • Logging and formatting: Use NestJS’s Logger to make important events easier to track
  • Unified error handling: Standardize exception handling with a custom ExceptionFilter
  • Introducing a test strategy: How to write unit tests for the Service layer and E2E tests via the Controller
  • Environment-specific configuration management: Use a dedicated .env.test and separate configurations

As a first step toward building a “trustworthy API” ready for production, the next part will focus on practical techniques to enhance application reliability.

https://shinagawa-web.com/en/blogs/nestjs-blog-series-logging-error-testing

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form