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?

3 Replies 3

I assume that you are applying Feature-based structure for your project as it is the default structure of NestJS. It is good for small project and for the starting. However, when your project grows too big, your concern becomes a real challenge. If I were you, I will think about moving to Clean Architecture and DDD.

@Pham even with DDD, still the problem exists. The user will use the interface with UI applications as well so that needs to be shared, the model is inevitable for the backend. The DTO is required for validations etc, It can be done with an interface in the controller but there cannot be validations attached.

Now If I have to add/remove/update any property. I have to touch 3 files, The interface, model and the DTO

@Pham @Joy Thank you both for your replies! You both make good points. Moving away from our basic CRUD implementation towards a design driven by specific use cases would greatly reduce the strain of maintaining the API, even though it would not solve the problem I posted about, as @Joy highlighted.

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.