Skip to content
C Codeloom
GraphQL

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.

·4 min read · By Codeloom
Intermediate 11 min read

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
GraphQL query flowing through resolvers into Prisma

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.