
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:
- We usedtsup for the packaging, it's a straightforward way to get it working
- Keep the dependencies of the packages neutral (no backend or frontend specific code)
Our implementation:
We useApiProperty 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 takesApiProperty 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";exportconstgetCreateUserDto=(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 clientconstApiProperty=ApiPropertySwagger||function(){};classCreateUserDto{@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;}returnCreateUserDto;};
Wait, does it work?
It does ! 🧙🏼♂️ ✨Dependency injection ✨
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`exportconst_CreateUserDto=getCreateUserDto(ApiProperty);// This allows using it as a TS type and as a constructor classexportclassCreateUserDtoextends_CreateUserDto{}
Client usage:
import{getCreateUserDto}from"@sample/dtos";// We don't need `ApiProperty` on the client,// so it will fallback on the default empty decoratorconst_CreateUserDto=getCreateUserDto();// This allows using it as a TS type and as a constructor classclassCreateUserDtoextends_CreateUserDto{}
Use the DTOs in the frontend:
If we go int NestJS implementation of DTOs, we'll see that they useclass-validator
, so we canuse 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";importgetfrom"lodash.get";import{useState}from"react";// We don't need `ApiProperty` on the client,// so it will fallback on the default empty decoratorconst_CreateUserDto=getCreateUserDto();// This allows using it as a TS type and as a constructor classclassCreateUserDtoextends_CreateUserDto{}constresolver=classValidatorResolver(CreateUserDto);exportdefaultfunctionWeb(){const{watch,register,handleSubmit,formState:{errors},}=useForm<CreateUserDto>({resolver,shouldFocusError:false,});constsubmitData=(validatedData:CreateUserDto)=>{postUser(validatedData);};const[nestResponse,setNestResponse]=useState<any>(null);constemailError=get(errors,"email.message");constfirstNameError=get(errors,"firstName.message");constlastNameError=get(errors,"lastName.message");constnationalityError=get(errors,"nationality.message");constnotValidatedData=watch();constpostUser=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(<ChakraProvidertheme={theme}><Flexas="form"onSubmit={handleSubmit(submitData)}noValidateflexDir={"column"}p={4}><FormControlisInvalid={Boolean(emailError)}><FormLabel>Email address</FormLabel><Inputtype="email"{...register("email")}/>{!emailError&&<FormHelperText>share your email.</FormHelperText>}<FormErrorMessage>{emailError}</FormErrorMessage></FormControl><FormControlisInvalid={Boolean(firstNameError)}><FormLabel>First Name</FormLabel><Input{...register("firstName")}/>{!firstNameError&&<FormHelperText>type your name.</FormHelperText>}<FormErrorMessage>{firstNameError}</FormErrorMessage></FormControl><FormControlisInvalid={Boolean(lastNameError)}><FormLabel>Last name</FormLabel><Input{...register("lastName")}/>{!lastNameError&&<FormHelperText>this is optional.</FormHelperText>}<FormErrorMessage>{lastNameError}</FormErrorMessage></FormControl><FormControlisInvalid={Boolean(nationalityError)}><FormLabel>Nationality</FormLabel><Input{...register("nationality")}/>{!nationalityError&&<FormHelperText>🇺🇾</FormHelperText>}<FormErrorMessage>{nationalityError}</FormErrorMessage></FormControl><Buttonbg="green.500"color="white"type="submit"w="fit-content"> Run DTO validation in the client + in the server</Button></Flex><Flexp="4"flexDir={"column"}><Buttonbg="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>);}
And that's it, Github repo:
(I'll be using this same markdown for the repo README.md)
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse