DEV Community

Cover image for FullStack NestJS DTOs for your web-app
Facundo Petre
Facundo Petre

Posted on

9

FullStack NestJS DTOs for your web-app

Use @nesjs DTOs on client & backend

I found no information about this, so here is a short description of my steps to get all of this together.

Advantages of packaging DTOs in a separate package:

  • Prevents duplication between multiple apps.
  • Consistency, if the DTO validations change, you'll get that changes in all your apps. So:
    • When you modify the expected payload for a given controller, this will be in sync with the clients using this package.
  • You can also use it on your clients, allowing frontends to run the same validations. That prevent unnecessary API calls.

Tips:

  1. We used tsup for the packaging, it's a straightforward way to get it working
  2. Keep the dependencies of the packages neutral (no backend or frontend specific code)

Our implementation:

We use ApiProperty from from @nestjs/swagger but this has a dependency with @nestjs/core and some extra stuff that is only server related.

Our way to solve this was to create a function that takes ApiProperty decorator as an optional parameter. We set the default value as an empty function (a no-effect decorator).

This is our implementation:

Packaged DTO (runs in front and backend):

import { IsEmail, IsOptional, IsString, MinLength } from "class-validator";

export const getCreateUserDto = (ApiPropertySwagger?: any) => {
  // We did this to avoid having to include all nest dependencies related to ApiProperty on the client side too
  // With this approach the value of this decorator will be injected by the server but wont affect the client
  const ApiProperty = ApiPropertySwagger || function () {};

  class CreateUserDto {
    @IsEmail()
    @ApiProperty({
      description: "This is required and must be a valid email",
      type: String,
    })
    email: string;

    @IsString()
    @MinLength(2)
    @ApiProperty({
      description: "This is required and must be at least 2 characters long",
      type: String,
    })
    firstName: string;

    @IsString()
    @IsOptional()
    lastName?: string;

    @IsString()
    @IsOptional()
    nationality?: string;
  }

  return CreateUserDto;
};
Enter fullscreen mode Exit fullscreen mode

Wait, does it work?

It does ! πŸ§™πŸΌβ€β™‚οΈ ✨ Dependency injection ✨

Here is an screenshot of the swagger api doc working

Backend usage:

After doing the following in tour DTO file, you can use it as any other NestJs DTO

import { getCreateUserDto } from '@sample/dtos';
import { ApiProperty } from '@nestjs/swagger';
// Here we send `ApiProperty` dependency to  be added to`CreateUserDto`
export const _CreateUserDto = getCreateUserDto(ApiProperty);

// This allows using it as a TS type and as a constructor class
export class CreateUserDto extends _CreateUserDto {} 
Enter fullscreen mode Exit fullscreen mode

Client usage:

import { getCreateUserDto } from "@sample/dtos";

// We don't need `ApiProperty` on the client,
// so it will fallback on the default empty decorator 
const _CreateUserDto = getCreateUserDto();
// This allows using it as a TS type and as a constructor class
class CreateUserDto extends _CreateUserDto {}
Enter fullscreen mode Exit fullscreen mode

Use the DTOs in the frontend:

If we go int NestJS implementation of DTOs, we'll see that they use class-validator, so we can use react-hook-forms + @hookform/resolvers/class-validator to use them as validators for our forms:

import { getCreateUserDto } from "@sample/dtos";
import { useForm } from "react-hook-form";
import { classValidatorResolver } from "@hookform/resolvers/class-validator";
import {
  FormControl,
  FormLabel,
  FormErrorMessage,
  FormHelperText,
  ChakraProvider,
  Flex,
  Input,
  Button,
  theme,
  Heading,
} from "@chakra-ui/react";
import get from "lodash.get";
import { useState } from "react";

// We don't need `ApiProperty` on the client,
// so it will fallback on the default empty decorator
const _CreateUserDto = getCreateUserDto();
// This allows using it as a TS type and as a constructor class
class CreateUserDto extends _CreateUserDto {}

const resolver = classValidatorResolver(CreateUserDto);

export default function Web() {
  const {
    watch,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<CreateUserDto>({
    resolver,
    shouldFocusError: false,
  });

  const submitData = (validatedData: CreateUserDto) => {
    postUser(validatedData);
  };

  const [nestResponse, setNestResponse] = useState<any>(null);

  const emailError = get(errors, "email.message");
  const firstNameError = get(errors, "firstName.message");
  const lastNameError = get(errors, "lastName.message");
  const nationalityError = get(errors, "nationality.message");

  const notValidatedData = watch();

  const postUser = async (user: CreateUserDto) => {
    fetch("http://localhost:4000/users", {
      method: "POST",
      mode: "cors",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(user),
    })
      .then((response) => response.json())
      .then((data) => {
        setNestResponse(data);
      });
  };

  return (
    <ChakraProvider theme={theme}>
      <Flex
        as="form"
        onSubmit={handleSubmit(submitData)}
        noValidate
        flexDir={"column"}
        p={4}
      >
        <FormControl isInvalid={Boolean(emailError)}>
          <FormLabel>Email address</FormLabel>
          <Input type="email" {...register("email")} />
          {!emailError && <FormHelperText>share your email.</FormHelperText>}
          <FormErrorMessage>{emailError}</FormErrorMessage>
        </FormControl>
        <FormControl isInvalid={Boolean(firstNameError)}>
          <FormLabel>First Name</FormLabel>
          <Input {...register("firstName")} />
          {!firstNameError && <FormHelperText>type your name.</FormHelperText>}
          <FormErrorMessage>{firstNameError}</FormErrorMessage>
        </FormControl>
        <FormControl isInvalid={Boolean(lastNameError)}>
          <FormLabel>Last name</FormLabel>
          <Input {...register("lastName")} />
          {!lastNameError && <FormHelperText>this is optional.</FormHelperText>}
          <FormErrorMessage>{lastNameError}</FormErrorMessage>
        </FormControl>
        <FormControl isInvalid={Boolean(nationalityError)}>
          <FormLabel>Nationality</FormLabel>
          <Input {...register("nationality")} />
          {!nationalityError && <FormHelperText>πŸ‡ΊπŸ‡Ύ</FormHelperText>}
          <FormErrorMessage>{nationalityError}</FormErrorMessage>
        </FormControl>

        <Button bg="green.500" color="white" type="submit" w="fit-content">
          Run DTO validation in the client + in the server
        </Button>
      </Flex>
      <Flex p="4" flexDir={"column"}>
        <Button
          bg="red"
          onClick={() => postUser(notValidatedData)}
          w="fit-content"
        >
          Run DTO only in the server
        </Button>
        <Heading>Controller response: </Heading>
        <pre>{JSON.stringify(nestResponse, null, 2)}</pre>
      </Flex>
    </ChakraProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's it, Github repo:

(I'll be using this same markdown for the repo README.md)

https://github.com/facundop3/poc-use-nestjs-dto-on-clients

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series πŸ“Ί

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series πŸ‘€

Watch the Youtube series