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.


  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 {
      description: "This is required and must be a valid email",
      type: String,
    email: string;

      description: "This is required and must be at least 2 characters long",
      type: String,
    firstName: string;

    lastName?: string;

    nationality?: string;

  return CreateUserDto;

Wait, does it work?

πŸ§™πŸΌβ€β™‚οΈ ✨ 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 {} 

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 {}

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 {
} 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 {
    formState: { errors },
  } = useForm<CreateUserDto>({
    shouldFocusError: false,

  const submitData = (validatedData: CreateUserDto) => {

  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) => {

  return (
    <ChakraProvider theme={theme}>
        <FormControl isInvalid={Boolean(emailError)}>
          <FormLabel>Email address</FormLabel>
          <Input type="email" {...register("email")} />
          {!emailError && <FormHelperText>share your email.</FormHelperText>}
        <FormControl isInvalid={Boolean(firstNameError)}>
          <FormLabel>First Name</FormLabel>
          <Input {...register("firstName")} />
          {!firstNameError && <FormHelperText>type your name.</FormHelperText>}
        <FormControl isInvalid={Boolean(lastNameError)}>
          <FormLabel>Last name</FormLabel>
          <Input {...register("lastName")} />
          {!lastNameError && <FormHelperText>this is optional.</FormHelperText>}
        <FormControl isInvalid={Boolean(nationalityError)}>
          <Input {...register("nationality")} />
          {!nationalityError && <FormHelperText>πŸ‡ΊπŸ‡Ύ</FormHelperText>}

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

And that’s it Github repo:

(I’ll be using this same markdown for the repo


View Github