We currently have a NestJS API using Prisma ORM. The API is used by multiple Next.js applications in different repos. To help with type safety across the various repos, we have a common "types" library which defines interfaces for all of our requests/responses.
Our Next.js applications use the shared types interfaces directly, occasionally building new types off of them for app-specific data shapes. Our NestJS API defines DTO classes which implement those interfaces as well as the interfaces generated automatically by Prisma.
I'm concerned this is not an ideal implementation for a few reasons:
Our DTOs are closely tied to our Prisma schema; changes to the latter mean changes to the former.
Our DTOs are very similar to the types library interfaces; it feels like we're doubling up on maintenance when we could just have a single representation for both of these.
- This was originally done because NestJS DTOs need to be concrete classes in order for various decorator-based functionality to work (e.g. OpenAPI plugin, validation pipe)
We've got a lot of repos involved, so even simple changes end up with multiple PRs.
- Nearly any change means updating our API repo (Prisma schema + DTOs) and our shared types repo (interfaces) at a minimum. Often, we also need to update both of our React app repos as well.
For example:
Prisma schema
model Lizard {
id String @id
name String? // new property
DTO
// `PrismaLizard` is an alias for the interface in the auto-generated Prisma client
// `SharedTypesLizard` is an alias for the interface in our shared types library
class Lizard implements PrismaLizard, SharedTypesLizard {
@ApiProperty({ type: String })
id: string;
@ApiProperty({ type: String, required: false })
name?: string; // new property
}
Shared types library interface
interface Lizard {
id: string,
name?: string // new property
}
EDIT
Removing the Prisma interface from the DTO eased a lot of the pains we were seeing.
Ironically, I originally added it as a convenient way to ensure that we weren't forgetting any fields when creating our DTOs (letting TypeScript warn us about them). The problem there was that the DTOs and Prisma schema were tightly coupled, which sort of defeats the purpose of the DTOs.
So now we have:
- Shared API types
- DTOs within the API application
- Custom types within the client applications
It seems to me that the generated Prisma client should only be used internally within the API, not shared to the client apps.
Thoughts?