Nest.js Tutorial

PUT and PATCH requests with PostgreSQL and Drizzle ORM

Marcin Wanago
NestJS

When users send an HTTP request to our API, they use a specific method to indicate whether they want to retrieve, send, delete, or update data. While we could technically delete data on a GET request, it’s our responsibility to design the API in a way that is intuitive and adheres to best practices.

Most HTTP methods are simple and self-explanatory. However, both the POST and PATCH methods can modify existing data, which can sometimes cause confusion. In this article, we’ll explore how to use them with Drizzle ORM and clarify their differences.

Our database contains a straightforward table with articles.

database-schema.ts
1import { serial, text, pgTable } from 'drizzle-orm/pg-core';
2 
3export const articles = pgTable('articles', {
4  id: serial('id').primaryKey(),
5  title: text('title').notNull(),
6  content: text('content'),
7});
8 
9export const databaseSchema = {
10  articles,
11};

Let’s start by fetching one of the existing articles.

1GET /articles/1
Response:
1{
2  "id": 1,
3  "title": "My first article",
4  "content": "Hello world!"
5}

One of the possible approaches to modifying existing articles is to use a PUT method. It changes the entities by replacing them. If the request does not include a particular field, the field should be removed.

1PUT /articles/1
Request body:
1{
2  "id": 1,
3  "title": "My first article, modified" 
4}
Response:
1{
2  "id": 1,
3  "title": "My first article, modified",
4  "content": null
5}

Since the request body does not contain the content property, our application should set it to null.

Implementing the PUT method with Drizzle ORM

We need to use the update method to modify existing entities with the Drizzle ORM.

1this.drizzleService.db
2  .update(databaseSchema.articles)
3  .set({
4    title: 'My first article!',
5  })
6  .where(eq(databaseSchema.articles.id, 1));

The crucial thing is that the update method affects only the properties we provide explicitly. To remove the content property, we need to explicitly set it to null.

1this.drizzleService.db
2  .update(databaseSchema.articles)
3  .set({
4    title: 'My first article!',
5    content: null,
6  })
7  .where(eq(databaseSchema.articles.id, 1));

To deal with this, we can create a Data Transfer Object that uses null as a default value.

replace-article.dto.ts
1import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2 
3export class ReplaceArticleDto {
4  @IsString()
5  @IsNotEmpty()
6  title: string;
7 
8  @IsString()
9  @IsNotEmpty()
10  @IsOptional()
11  content: string | null = null;
12}

Now, we can use the above DTO in our service. If the user doesn’t provide one of the optional properties, our API will set them to null.

articles.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5import { ReplaceArticleDto } from './dto/replace-article.dto';
6 
7@Injectable()
8export class ArticlesService {
9  constructor(private readonly drizzleService: DrizzleService) {}
10 
11  async replace(id: number, article: ReplaceArticleDto) {
12    const updatedArticles = await this.drizzleService.db
13      .update(databaseSchema.articles)
14      .set({
15        title: article.title,
16        content: article.content,
17      })
18      .where(eq(databaseSchema.articles.id, id))
19      .returning();
20 
21    if (updatedArticles.length === 0) {
22      throw new NotFoundException();
23    }
24 
25    return updatedArticles.pop();
26  }
27 
28  // ...
29}

Finally, we can create a PUT method handler in our controller.

articles.controller.ts
1import { Body, Controller, Param, ParseIntPipe, Put } from '@nestjs/common';
2import { ArticlesService } from './articles.service';
3import { ReplaceArticleDto } from './dto/replace-article.dto';
4 
5@Controller('articles')
6export class ArticlesController {
7  constructor(private readonly articlesService: ArticlesService) {}
8 
9  @Put(':id')
10  replace(
11    @Param('id', ParseIntPipe) id: number,
12    @Body() article: ReplaceArticleDto,
13  ) {
14    return this.articlesService.replace(id, article);
15  }
16 
17  // ...
18}

Thanks to this approach, making a PUT request without providing the content property changes it to null.

PATCH

Alternatively, we can use the PATCH method to modify existing entities partially using a set of instructions. The most common way of implementing the PATCH method is to handle a request body with a partial entity.

1PATCH /articles/2
Request body:
1{
2  "id": 2,
3  "title": "My second article, modified"
4}
Response:
1{
2  "id": 2,
3  "title": "My second article, modified",
4  "content": "Hello world!"
5}

The most important thing about the PATCH requests is that to delete a property, we must send the null value explicitly. Because of that, the above request does not remove the content property. This prevents us from removing values by accident.

Implementing the PATCH method with Drizzle ORM

With the PATCH method, providing any of the properties is optional. Because of that, we might think of using the @IsOptional() decorator built into the class-validator library. Unfortunately, there is a significant issue with it. It allows the property to be either undefined or null, which can be confusing.

While all properties are optional when using a PATCH method, trying to set null for a property that is not nullable can cause an Internal Server Error in our application. To solve this, we should use the @ValidateIf() decorator to allow the users to omit a value but not provide null.

update-article.dto.ts
1import { IsString, IsNotEmpty, IsOptional, ValidateIf } from 'class-validator';
2 
3export class UpdateArticleDto {
4  @IsString()
5  @IsNotEmpty()
6  @ValidateIf((object, value) => value !== undefined)
7  title?: string;
8 
9  @IsString()
10  @IsNotEmpty()
11  @IsOptional()
12  content?: string | null;
13}
We can still use the @IsOptional() decorator for content because this property is nullable in our database.

With the above approach, we validate the title property only if the user provides it. If the user does not provide the title property, we don’t throw an error.

We can create a custom decorator that uses ValidateIf() under the hood to prevent our code from being duplicated.

can-be-undefined.ts
1import { ValidateIf } from 'class-validator';
2 
3export function CanBeUndefined() {
4  return ValidateIf((data, value) => value !== undefined);
5}

Thanks to the above decorator, our code can be shorter and more explicit.

update-article.dto.ts
1import { IsString, IsNotEmpty, IsOptional } from 'class-validator';
2import { CanBeUndefined } from '../../utilities/can-be-undefined';
3 
4export class UpdateArticleDto {
5  @IsString()
6  @IsNotEmpty()
7  @CanBeUndefined()
8  title?: string;
9 
10  @IsString()
11  @IsNotEmpty()
12  @IsOptional()
13  content?: string | null;
14}

To implement the PATCH method with Drizzle ORM, we need to use the update method like before. It fits our needs since it does not modify the properties the user didn’t explicitly provide.

articles.service.ts
1import { Injectable, NotFoundException } from '@nestjs/common';
2import { DrizzleService } from '../database/drizzle.service';
3import { databaseSchema } from '../database/database-schema';
4import { eq } from 'drizzle-orm';
5import { UpdateArticleDto } from './dto/update-article.dto';
6 
7@Injectable()
8export class ArticlesService {
9  constructor(private readonly drizzleService: DrizzleService) {}
10 
11  async update(id: number, article: UpdateArticleDto) {
12    const updatedArticles = await this.drizzleService.db
13      .update(databaseSchema.articles)
14      .set({
15        title: article.title,
16        content: article.content,
17      })
18      .where(eq(databaseSchema.articles.id, id))
19      .returning();
20 
21    if (updatedArticles.length === 0) {
22      throw new NotFoundException();
23    }
24 
25    return updatedArticles.pop();
26  }
27 
28  // ...
29}

The last step is to add the PATCH method to our controller.

articles.controller.ts
1import { Body, Controller, Param, ParseIntPipe, Patch } from '@nestjs/common';
2import { ArticlesService } from './articles.service';
3import { UpdateArticleDto } from './dto/update-article.dto';
4 
5@Controller('articles')
6export class ArticlesController {
7  constructor(private readonly articlesService: ArticlesService) {}
8 
9  @Patch(':id')
10  update(
11    @Param('id', ParseIntPipe) id: number,
12    @Body() article: UpdateArticleDto,
13  ) {
14    return this.articlesService.update(id, article);
15  }
16 
17  // ...
18}

Summary

In this article, we’ve explained how to modify existing entities using both the PUT and PATCH methods using NestJS and the Drizzle ORM. To achieve that, we had to dive deeper into how the class-validator handles missing values.

While we can make both approaches work, using PATCH prevents the users from accidentally removing properties since they need to provide the null value explicitly. Understanding the difference between PUT and PATCH methods is important if we want our API to be user-friendly and to follow best practices.