GraphQL with Prisma Tutorial
A practical guide to wiring GraphQL on top of Prisma. Schema design, resolvers, the N+1 problem, batching, and the patterns that keep your API fast as it grows.
What you'll learn
- ✓How GraphQL and Prisma fit together
- ✓How to model schemas in both layers
- ✓How resolvers map to Prisma queries
- ✓Why N+1 happens and how to fix it
- ✓Patterns that scale beyond a hobby project
Prerequisites
- •Basic Node.js
- •Some SQL exposure
GraphQL gives clients a flexible query language. Prisma gives your server a typed database client. Put them together and you get an API where the schema, the resolvers, and the database queries all line up. This tutorial walks through the wiring and the traps.
What and Why
GraphQL is a query layer at the edge of your service. Clients ask for exactly the fields they want, and the server returns that shape. Prisma is an ORM that turns a database schema into a typed client. The two are not the same schema, even though they look similar. The GraphQL schema is the API contract. The Prisma schema is the database contract. You write resolvers to translate between them.
The reason to combine them is simple. Prisma makes data access safe and ergonomic. GraphQL makes the API expressive. Together they eliminate a huge amount of boilerplate that would otherwise live in controllers and serializers.
Mental Model
Think of three layers stacked on top of each other. At the top sits the GraphQL schema, defining types like User and Post and the queries that return them. In the middle sit resolvers, small functions that handle each field. At the bottom sits Prisma, which talks to the database. A query flows down: the client sends a GraphQL document, the server picks the right resolvers, and Prisma builds the SQL.
The most important thing to internalise is that one GraphQL query can trigger many resolver calls, and each resolver can trigger its own database query. Without care, that becomes a flood.
Hands-on Example
Start with a Prisma schema for users and posts.
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}
Then a GraphQL schema and resolvers using a tool like GraphQL Yoga.
const typeDefs = `
type User { id: Int!, email: String!, posts: [Post!]! }
type Post { id: Int!, title: String!, author: User! }
type Query { users: [User!]! }
`;
const resolvers = {
Query: {
users: (_, __, { prisma }) => prisma.user.findMany(),
},
User: {
posts: (user, _, { prisma }) =>
prisma.post.findMany({ where: { authorId: user.id } }),
},
};
A query for ten users with their posts looks fine but actually runs eleven database queries: one for users and one per user for posts. That is the N+1 problem.
client query
|
v
[GraphQL schema]
|
v
[Query.users resolver] --> prisma.user.findMany
|
v
[User.posts resolver] x N --> prisma.post.findMany (per user)
|
v
JSON response The fix is a DataLoader or Prisma’s findMany with an in filter that batches the lookups into one query keyed by authorId.
Common Pitfalls
The first pitfall is treating the GraphQL schema as a mirror of the Prisma schema. They drift, and they should. Hide internal fields. Rename for clarity. Compose aggregates that do not exist in the database.
The second is forgetting authorization. Prisma will happily return anything you ask for. Resolvers need to check the current user before returning data, and that check has to run for nested fields too.
The third is over-fetching from the database. Just because the resolver returns a User object does not mean you need every column. Use Prisma’s select to fetch only what the query actually needs.
Practical Tips
Generate types from your GraphQL schema and use them in your resolvers. The compile-time guarantee that resolvers return the right shape is worth the setup.
Push filtering, sorting, and pagination into Prisma instead of doing it in JavaScript. The database is faster, and the network payload is smaller.
Log every resolver that issues a query during development. If a single client query produces more than a handful of database calls, batch them.
Wrap-up
GraphQL with Prisma is a productive stack when you respect the two-schema boundary and treat the resolver layer as the place where translation, authorization, and batching happen. Get those right and the rest of the API tends to follow.
Related articles
- GraphQL GraphQL Rate Limiting Strategies
Why GraphQL rate limiting is harder than REST and what to do about it. Covers query complexity analysis, depth limits, cost-based budgets, and per-field throttling.
- GraphQL GraphQL Schema Design Best Practices
Practical patterns for designing GraphQL schemas that age well: nullability, pagination, mutations, errors, and evolution without versioning.
- GraphQL GraphQL vs REST Tradeoffs
An honest comparison of GraphQL and REST: where each one shines, where each one hurts, and how to choose without religion.
- Next.js Next.js Route Handlers vs API Routes
Understand the difference between Pages Router API routes and App Router route handlers, including request, response, and runtime options.