Will Despard
Will Despard

Reputation: 517

"Single source of truth" schema/validation class for server (NestJs/GraphQL/TypeGraphQL/Prisma) and client (NextJs)

I'm building an Typescript application using an NX monorepo, NestJs/GraphQL/TypeGraphQL (built into @nestjs/graphql)/Prisma/PostgreSQL on the server, and NextJs on the client.

With this stack (when using the code first approach for GraphQL) you need to provide the following schemas for each entity (for example, 'User'):

  1. Prisma model schema (written in Prisma's own Prisma Schema Language), used to model the database:
model User {
  id Int @id @default(autoincrement())
  name String
  email String @unique
  password String
  createdAt DateTime @default(now()) @map("created_at")
  @@map("users")
}
  1. GraphQL returned entity schema (used to define what is returned by a resolver. The decorators are from TypeGraphQL, but are imported from @nestjs/graphql):
import { Field, ObjectType, ID } from '@nestjs/graphql';

@ObjectType()
export class User {
  @Field(() => ID)
  id: number;

  @Field()
  name: string;

  @Field()
  email: string;

  @Field()
  password: string;

  @Field()
  createdAt: Date;
}
  1. GraphQL DTO read/input schema (used to determine what fields are required/validated when someone queries your /graphql endpoint). As you can see I'm using the 'class-validator' package to perform validation using decorators:
import { InputType, Field } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty } from 'class-validator';

@InputType()
export class SignupInput {
  @Field()
  @IsNotEmpty()
  name: string;

  @Field()
  @IsNotEmpty()
  @IsEmail()
  email: string;

  @Field()
  @IsNotEmpty()
  password: string;
}
  1. Client (NextJs) form input validation schema. On the client, I'm using React Hook Form, and passing a validation class, using the 'class-validator' package, to it:
class SignupForm {
  @IsNotEmpty({ message: 'Name is a required' })
  name: string;

  @IsNotEmpty({ message: 'Email is required' })
  @IsEmail({}, { message: 'This is not an email' })
  email: string;

  @IsNotEmpty({ message: 'Password is required' })
  password: string;
}

As you can see there is a LOT of overlap between these different schemas. In the spirit of keeping my code DRY, I was hoping of finding a way of sharing a single schema between all the different parts of my code.

Regarding Prisma, I think this is a lost battle, they require you to write your models in their Prisma Schema Language. There is a package that makes working with TypeGraphQL easier, but it appears not to be working with NestJs. If anyone knows anything different, please say!

Regarding TypeGraphQL, they do have a section in their docs about how to use their schema in the browser, which means it could be shared with my NextJs client. However, after hours of trying, I've been unable to get this working. Mainly, I think this is either because I'm using the TypeGraphQL provided by @nestjs/graphql or becuase of the way my NX monorepo handles Webpack config.

Some helpful links:

https://github.com/MichalLytek/type-graphql/issues/100

https://nextjs.org/docs/api-reference/next.config.js/custom-webpack-config

https://github.com/nrwl/nx/issues/3175

https://yonatankra.com/how-to-use-custom-webpack-configuration-in-a-nrwl-project/

Any help would be much appreciated.

Upvotes: 2

Views: 1375

Answers (2)

Aerodynamic
Aerodynamic

Reputation: 819

A "single source of truth" for both front and serverside data validation is also something I have been, for a long time, searching for.

I recently learned about "community generators" for Prisma, which allow you to generate schemas for joi, (also yup, class validator, ...) from your Prisma model.

In theory, you should be able to use these for validation of your front end forms. Not sure yet how feature complete they are though.

https://www.prisma.io/docs/concepts/components/prisma-schema/generators#community-generators

prisma-json-schema-generator: @grimen prisma-joi-generator: Generate full Joi schemas from your Prisma schema.

Source: https://github.com/prisma/prisma/issues/3528#issuecomment-1133879401

Upvotes: 0

Noam
Noam

Reputation: 5256

Note: I’m one of the authors of Remult

With Remult you have one TypeScript class (per entity) which serves as a single source of truth for the API endpoints, API client and ORM. It can also run TypeScript validation code, defined in entity decorators, in both the frontend (e.g. before mutation requests are sent) and the backend.

Only caveat is that while the current version of Remult can be used to expose a GraphQL endpoint, the current API client only supports RESTful. Other than that I think it can fit nicely in your stack.

Upvotes: 1

Related Questions