Resolvers
Master GraphQL resolvers with defineResolver, defineQuery, defineMutation, defineType, and defineSubscription.
What are Resolvers?
Resolvers are functions that return data for GraphQL fields. They connect your schema to your data sources (databases, APIs, files, etc.).
Resolver Functions
All resolver utility functions are auto-imported in .resolver.ts files. They are exported from nitro-graphql/utils/define.
defineResolver
Define a complete resolver map with Query, Mutation, and custom types:
// server/graphql/app.resolver.ts
export const appResolver = defineResolver({
Query: {
hello: () => 'Hello World',
user: (_, { id }) => findUser(id),
},
Mutation: {
createUser: (_, { input }) => createUser(input),
},
User: {
posts: parent => findPostsByUser(parent.id),
},
})defineQuery
Define only Query resolvers:
// server/graphql/users/queries.resolver.ts
export const userQueries = defineQuery({
users: async (_, __, context) => {
return await context.storage?.getItem('users') || []
},
user: async (_, { id }, context) => {
const users = await context.storage?.getItem('users') || []
return users.find(u => u.id === id) || null
},
})defineMutation
Define only Mutation resolvers:
// server/graphql/users/mutations.resolver.ts
export const userMutations = defineMutation({
createUser: async (_, { input }, context) => {
const users = await context.storage?.getItem('users') || []
const user = {
id: Date.now().toString(),
...input,
createdAt: new Date(),
}
users.push(user)
await context.storage?.setItem('users', users)
return user
},
updateUser: async (_, { id, input }, context) => {
const users = await context.storage?.getItem('users') || []
const index = users.findIndex(u => u.id === id)
if (index === -1)
return null
users[index] = { ...users[index], ...input }
await context.storage?.setItem('users', users)
return users[index]
},
deleteUser: async (_, { id }, context) => {
const users = await context.storage?.getItem('users') || []
const filtered = users.filter(u => u.id !== id)
await context.storage?.setItem('users', filtered)
return filtered.length < users.length
},
})defineType
Define field resolvers for custom types:
// server/graphql/users/types.resolver.ts
export const userTypes = defineType({
User: {
// Resolve the posts field
posts: async (parent, _, context) => {
const posts = await context.storage?.getItem('posts') || []
return posts.filter(p => p.authorId === parent.id)
},
// Resolve the fullName field
fullName: (parent) => {
return `${parent.firstName} ${parent.lastName}`
},
// Async field resolver
avatar: async (parent) => {
return await fetchAvatar(parent.email)
},
},
})defineSubscription
Define WebSocket subscriptions:
// server/graphql/messages/subscriptions.resolver.ts
export const messageSubscriptions = defineSubscription({
messageAdded: {
subscribe: (_, __, context) => {
return context.pubsub.asyncIterator(['MESSAGE_ADDED'])
},
resolve: payload => payload,
},
userTyping: {
subscribe: (_, { channelId }, context) => {
return context.pubsub.asyncIterator([`TYPING_${channelId}`])
},
},
})Learn More
See the Subscriptions Guide for detailed WebSocket setup.
Resolver Arguments
All resolver functions receive four arguments:
function resolver(parent, args, context, info) {
// ...
}1. parent
The return value from the parent field resolver:
// Schema
type User {
id: ID!
posts: [Post!]!
}
// Resolvers
export const userQueries = defineQuery({
user: () => ({
id: '1',
name: 'John',
// ... user data
}),
})
export const userTypes = defineType({
User: {
posts: (parent) => {
// parent is the User object from the query resolver
console.log(parent.id) // '1'
console.log(parent.name) // 'John'
return findPostsByUser(parent.id)
},
},
})2. args
Arguments passed to the field:
export const userQueries = defineQuery({
user: (_, args) => {
console.log(args.id) // The id argument
return findUser(args.id)
},
users: (_, args) => {
console.log(args.limit) // 10
console.log(args.offset) // 0
return findUsers(args)
},
})With destructuring:
export const userQueries = defineQuery({
user: (_, { id }) => findUser(id),
users: (_, { limit = 10, offset = 0 }) => findUsers(limit, offset),
})3. context
The H3 Event context object:
export const userQueries = defineQuery({
me: async (_, __, context) => {
// Access H3 event
const event = context.event
// Access storage
const users = await context.storage?.getItem('users')
// Access custom context
const userId = context.auth?.userId
// Use server-side fetch
const data = await context.$fetch('/api/data')
return findUser(userId)
},
})Learn More About Context
See the Context Guide for details on extending and using context.
4. info
Field execution information (rarely used):
export const userQueries = defineQuery({
users: (_, __, ___, info) => {
console.log(info.fieldName) // 'users'
console.log(info.returnType) // [User!]!
console.log(info.parentType) // Query
console.log(info.path) // Path to this field
// Useful for optimization - check what fields were requested
const requestedFields = info.fieldNodes[0].selectionSet?.selections
return findUsers()
},
})Async Resolvers
Most resolvers are async for database/API calls:
export const postQueries = defineQuery({
posts: async () => {
const db = await useDatabase()
return await db.post.findMany()
},
post: async (_, { id }) => {
const db = await useDatabase()
return await db.post.findUnique({ where: { id } })
},
})
export const postMutations = defineMutation({
createPost: async (_, { input }) => {
const db = await useDatabase()
return await db.post.create({ data: input })
},
})Field Resolvers
Resolve nested fields with field resolvers:
type Post {
id: ID!
title: String!
author: User! # ← Needs resolver
comments: [Comment!]! # ← Needs resolver
likeCount: Int! # ← Computed field
}export const postTypes = defineType({
Post: {
// Resolve author relationship
author: async (parent, _, context) => {
const users = await context.storage?.getItem('users') || []
return users.find(u => u.id === parent.authorId)
},
// Resolve comments relationship
comments: async (parent, _, context) => {
const comments = await context.storage?.getItem('comments') || []
return comments.filter(c => c.postId === parent.id)
},
// Computed field
likeCount: (parent) => {
return parent.likes?.length || 0
},
},
})DataLoader Pattern
Prevent N+1 queries with DataLoader:
import DataLoader from 'dataloader'
// Create a DataLoader
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } }
})
// Return in the same order as requested
return userIds.map(id => users.find(u => u.id === id))
})
export const postTypes = defineType({
Post: {
// Use DataLoader - batches and caches requests
author: async (parent) => {
return await userLoader.load(parent.authorId)
},
},
})Performance
See the Performance Guide for advanced optimization techniques.
Error Handling
Throw GraphQL errors from resolvers:
import { GraphQLError } from 'graphql'
export const postMutations = defineMutation({
deletePost: async (_, { id }, context) => {
const post = await findPost(id)
if (!post) {
throw new GraphQLError('Post not found', {
extensions: {
code: 'NOT_FOUND',
id,
},
})
}
if (post.authorId !== context.auth?.userId) {
throw new GraphQLError('Not authorized', {
extensions: {
code: 'FORBIDDEN',
},
})
}
await deletePost(id)
return true
},
})Error Handling Guide
See the Error Handling Guide for patterns and best practices.
Type Safety
Use generated types for full type safety:
import type { CreateUserInput, Resolvers, User } from '#graphql/server'
export const userMutations = defineMutation({
createUser: async (
_,
{ input }: { input: CreateUserInput },
context
): Promise<User> => {
const user: User = {
id: Date.now().toString(),
name: input.name,
email: input.email,
createdAt: new Date(),
}
return user
},
})TIP
The resolver functions themselves are already typed! The return type of defineMutation is Resolvers, which includes all type information from your schema.
Combining Multiple Resolvers
Split resolvers across files - they're automatically merged:
// server/graphql/users/queries.resolver.ts
export const userQueries = defineQuery({
users: () => [...],
user: () => ({...}),
})// server/graphql/users/mutations.resolver.ts
export const userMutations = defineMutation({
createUser: () => ({...}),
updateUser: () => ({...}),
})// server/graphql/users/types.resolver.ts
export const userTypes = defineType({
User: {
posts: () => [...],
},
})// server/graphql/posts/queries.resolver.ts
export const postQueries = defineQuery({
posts: () => [...],
post: () => ({...}),
})All four resolvers are discovered and merged automatically!
Best Practices
1. Use Named Exports
// ✅ Correct
export const userQueries = defineQuery({...})
export const userMutations = defineMutation({...})
// ❌ Wrong - won't be discovered
export default defineQuery({...})2. Keep Resolvers Focused
// ✅ Good - Single responsibility
export const userQueries = defineQuery({
users: () => getAllUsers(),
user: (_, { id }) => getUser(id),
})
export const userMutations = defineMutation({
createUser: (_, { input }) => createUser(input),
})
// ❌ Bad - Too much in one file
export const everythingResolver = defineResolver({
Query: { /* 50 queries */ },
Mutation: { /* 50 mutations */ },
User: { /* many fields */ },
Post: { /* many fields */ },
// ...
})3. Extract Business Logic
// ✅ Good - Logic separated
async function createUserAccount(input: CreateUserInput) {
// Validation
if (!isValidEmail(input.email)) {
throw new Error('Invalid email')
}
// Business logic
const user = await db.user.create({ data: input })
await sendWelcomeEmail(user.email)
return user
}
export const userMutations = defineMutation({
createUser: (_, { input }) => createUserAccount(input),
})
// ❌ Bad - Everything in resolver
export const userMutations = defineMutation({
createUser: async (_, { input }) => {
if (!isValidEmail(input.email))
throw new Error('Invalid email')
const user = await db.user.create({ data: input })
await sendWelcomeEmail(user.email)
return user
},
})4. Use Context for Shared Resources
// ✅ Good - Use context
export const userQueries = defineQuery({
users: async (_, __, context) => {
const db = context.db // Shared DB connection
return await db.user.findMany()
},
})
// ❌ Bad - Create new connection each time
export const userQueries = defineQuery({
users: async () => {
const db = await createDbConnection()
return await db.user.findMany()
},
})5. Handle Null Cases
// ✅ Good - Explicit null handling
export const userQueries = defineQuery({
user: async (_, { id }, context) => {
const user = await context.db.user.findUnique({ where: { id } })
return user || null // Explicit null for not found
},
})
// ❌ Bad - Undefined can cause issues
export const userQueries = defineQuery({
user: async (_, { id }, context) => {
return await context.db.user.findUnique({ where: { id } })
// Returns undefined if not found
},
})Real-World Example
Here's a complete example with database, auth, and error handling:
import type { CreatePostInput, Post, UpdatePostInput } from '#graphql/server'
// server/graphql/posts/post.resolver.ts
import { GraphQLError } from 'graphql'
export const postQueries = defineQuery({
posts: async (_, { limit = 10, offset = 0 }, context) => {
return await context.db.post.findMany({
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' },
})
},
post: async (_, { id }, context) => {
const post = await context.db.post.findUnique({
where: { id },
})
if (!post) {
throw new GraphQLError('Post not found', {
extensions: { code: 'NOT_FOUND' },
})
}
return post
},
myPosts: async (_, __, context) => {
const userId = context.auth?.userId
if (!userId) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
return await context.db.post.findMany({
where: { authorId: userId },
})
},
})
export const postMutations = defineMutation({
createPost: async (_, { input }, context) => {
const userId = context.auth?.userId
if (!userId) {
throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' },
})
}
const post = await context.db.post.create({
data: {
...input,
authorId: userId,
},
})
return post
},
updatePost: async (_, { id, input }, context) => {
const post = await context.db.post.findUnique({ where: { id } })
if (!post) {
throw new GraphQLError('Post not found', {
extensions: { code: 'NOT_FOUND' },
})
}
if (post.authorId !== context.auth?.userId) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
})
}
return await context.db.post.update({
where: { id },
data: input,
})
},
})
export const postTypes = defineType({
Post: {
author: async (parent, _, context) => {
return await context.db.user.findUnique({
where: { id: parent.authorId },
})
},
comments: async (parent, _, context) => {
return await context.db.comment.findMany({
where: { postId: parent.id },
})
},
isLiked: async (parent, _, context) => {
const userId = context.auth?.userId
if (!userId)
return false
const like = await context.db.like.findFirst({
where: {
postId: parent.id,
userId,
},
})
return !!like
},
},
})