Extending Types for Prisma Extensions in NextJS
If you read my previous post, you’ll note that I was very excited to try out Prisma’s new Extensions alongside their Middleware and try out various use cases. When test driving I did hit a snag rebuilding the Types for our new extension to pull through.
If you are running Prisma with NextJS you’ve probably followed this initial best practice doc: https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
Here we define prisma
in the global
object so that we don’t create a bazillion instances of Prisma and have the world crashing down on us. If you are using Typescript, this global
instance was probably Typed. in our case we had:
declare global { // eslint-disable-next-line no-var var prisma: PrismaClient | undefined; }
The Issue
The issue is that the type PrismaClient
has no clue about our new Extensions or the types within. In this use case, we are attempting to create a new signUp
method on the User
model.
const extendedPrisma = prisma.$extends({ model: { user: { async signUp(args: SignUpArgs): Promise<UserWithRelations> { return await prisma.user.create({ ...args, }); }, }, }, });
So how do we fix this?…
The Solution
Turn const prisma
from the previous example into a function that returns the newly created extendedPrisma
context. When setting the global prisma value we use this value and Type instead. Now when we import prisma
it should include our new model action signUp
and type validations as seen below.
The Code
import { Prisma, PrismaClient, Profile, User, } from '@prisma/client'; export const defaultUserSessionIncludes = { profile: true, } as Prisma.UserCreateArgs['include']; export type UserWithRelations = User & { profile: Profile; }; // Set default prisma logs. More logs in debug mode. const logOptions: Prisma.LogLevel[] = process.env.DEBUG ? ['query', 'error'] : ['error']; type UserCreateArgsWithProfile = Omit<Prisma.UserCreateArgs['data'], 'profile'> & { // Require Profile for signup profile: Prisma.UserCreateArgs['data']['profile']; }; // mimics Prisma.UserCreateArgs type SignUpArgs = { data: UserCreateArgsWithProfile; include?: Prisma.UserCreateArgs['include']; }; const extendedPrismaClient = () => { const prisma = new PrismaClient({ log: logOptions, }); const extendedPrisma = prisma.$extends({ model: { user: { async signUp(args: SignUpArgs): Promise<UserWithRelations> { return await prisma.user.create({ ...args, }); }, }, }, }); return extendedPrisma; }; export type ExtendedPrismaClient = ReturnType<typeof extendedPrismaClient>; /** * Instantiate prisma client for Next.js: * https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#solution */ declare global { // eslint-disable-next-line no-var var prisma: ExtendedPrismaClient | undefined; } export const prisma = global.prisma || extendedPrismaClient(); if (process.env.NODE_ENV !== 'production') { global.prisma = prisma; } export async function disconnect() { await prisma.$disconnect(); return true; } export async function connect() { await prisma.$connect(); return true; }
If you are using Next-Auth
One other note if you are using Next-Auth.
We kept hitting a snag with the types for Next-Auth’s PrismaAdapter
Again, it expects a type of PrismaClient
but we want to pass our new ExtendedPrismaClient
type instead.
There was a weird type of error trying to recast this for some reason complaining about Prisma’s $use
missing on our type. Remembering this is still in the Experimental Phase. We didn’t try to dive in too hard here, rather we cast as unknown
first before passing along the types expected for the adapter. While not ideal, it does work and is something we’ll monitor as Prisma continues to wrap this feature up.
export const authOptions: NextAuthOptions = { adapter: CustomPrismaAdapter(prisma as unknown as ExtendedPrismaClient), ... } function CustomPrismaAdapter(p: ExtendedPrismaClient) { const prismaAdapter = PrismaAdapter(p as unknown as PrismaClient); ... }
That’s all folks!
Remember to restart that Typescript Server folks. 🙂 I kept battling this a few times only to realize I needed to “turn it off and on again”. Once set, you’ll be ready to keep tacking on new extensions and middleware!