Prisma plugin
Define GraphQL types from Prisma models and resolve relations with automatically optimized queries.
The Prisma plugin builds GraphQL object types straight from your Prisma models and resolves their relations with queries it plans for you. You call builder.prismaObject with a model name, expose the columns you want, and add relation fields with t.relation — the plugin turns a nested GraphQL selection into as few Prisma queries as it can, which is where the classic N+1 problem usually creeps in.
The plugin is not required to use Prisma with Pothos, but it removes a lot of manual wiring and query planning. If you would rather keep Prisma at arm's length, see Using Prisma without a plugin.
What it does
- Define GraphQL types from your Prisma models with full type-safety, without hand-writing object refs or importing generated client types.
- Resolve relations automatically from the relationships already declared in your database.
- Load exactly the data a query needs in as few round-trips as possible, folding nested relations into a single Prisma query where it can.
- Keep GraphQL type and field names independent of your column names and types.
- Integrate with the Relay plugin for nodes and connections that load efficiently.
- Back multiple GraphQL types with the same database model through variants.
- Add relation count fields to objects and connections.
An example
Here is a slice of an Ultimate League schema, built against the canonical Prisma schema. It defines a Team, exposes columns, computes a field from a related table, loads relations, and adds a Relay connection:
// A GraphQL type backed by the Team model — no object ref, no client imports.
builder.prismaObject('Team', {
fields: (t) => ({
id: t.exposeID('id'),
name: t.exposeString('name'),
// Load a related column on demand and compute a field from it.
starPlayer: t.string({
select: {
players: { orderBy: { number: 'asc' }, take: 1 },
},
resolve: (team) => team.players[0]?.name ?? 'TBD',
}),
// A list relation, with an argument that shapes the relation query.
players: t.relation('players', {
args: {
byNumber: t.arg.boolean(),
},
query: (args) => ({
orderBy: args.byNumber ? { number: 'asc' } : { name: 'asc' },
}),
}),
// A Relay connection using Prisma's cursor-based pagination.
gamesConnection: t.relatedConnection('homeGames', {
cursor: 'id',
}),
}),
});
// A Relay node backed by the Game model.
builder.prismaNode('Game', {
id: { field: 'id' },
fields: (t) => ({
playedAt: t.string({ resolve: (game) => game.playedAt.toISOString() }),
homeTeam: t.relation('homeTeam'),
}),
});
builder.queryType({
fields: (t) => ({
// A field that issues a single optimized Prisma query.
myTeam: t.prismaField({
type: 'Team',
resolve: async (query, _root, _args, ctx) =>
prisma.team.findUniqueOrThrow({
// Spreading `query` adds the include/select the plugin computed
// for the nested selection, resolving as much as possible at once.
...query,
where: { id: ctx.teamId },
}),
}),
}),
});How the query plan works
Given the schema above, a nested query resolves in a single Prisma call (which Prisma turns into a handful of optimized SQL statements):
query {
myTeam {
name
players {
name
stats {
goals
}
}
}
}The myTeam resolver receives a query with the include/select needed to load players and their stats in one go. Add a second, differently-argumented copy of the same relation, though, and one query is no longer enough:
query {
myTeam {
name
players {
name
}
byNumber: players(byNumber: true) {
name
}
}
}This runs two Prisma queries: one for everything except byNumber, and a second for the aliased relation. Prisma can resolve a given relation only once per query, so a second copy of players with different arguments needs its own query. The plugin detects this and splits the work into the fewest queries possible. Relations covers the edge cases that trigger a split.