Skip to content

Context

Learn how to work with H3EventContext in your GraphQL resolvers, add custom context properties, and access request data.

What is Context?

Context is an object passed to every resolver function as the third argument. It contains:

  • H3 Event: The raw H3 request event
  • Storage: Nitro's storage layer
  • Custom Properties: Anything you add (database, auth, etc.)

Accessing Context

ts
export const userQueries = defineQuery({
  users: async (parent, args, context) => {
    // context is H3EventContext
    console.log(context)
  },
})

Built-in Context Properties

event

The raw H3 event object:

ts
export const userQueries = defineQuery({
  me: async (_, __, context) => {
    const event = context.event

    // Request headers
    const token = getHeader(event, 'authorization')

    // Request body
    const body = await readBody(event)

    // Cookies
    const sessionId = getCookie(event, 'session')

    // IP address
    const ip = getRequestIP(event)

    return findUserByToken(token)
  },
})

storage

Nitro's storage layer (unstorage):

ts
export const userQueries = defineQuery({
  users: async (_, __, context) => {
    // Get from storage
    const users = await context.storage?.getItem('users') || []

    return users
  },
})

export const userMutations = defineMutation({
  createUser: async (_, { input }, context) => {
    const users = await context.storage?.getItem('users') || []
    const user = { id: Date.now().toString(), ...input }
    users.push(user)

    // Save to storage
    await context.storage?.setItem('users', users)

    return user
  },
})

Extending Context

Define Custom Context

Create or edit server/graphql/context.ts:

ts
// server/graphql/context.ts
import type { Database } from '../utils/db'

declare module 'h3' {
  interface H3EventContext {
    // Database connection
    db: Database

    // Authentication
    auth?: {
      userId: string
      role: 'admin' | 'moderator' | 'user'
    }

    // Custom services
    emailService: EmailService
    storageService: StorageService
  }
}

TypeScript Only

The context.ts file only provides TypeScript types. You must populate the context in middleware or plugins.

Populate Context

Use Nitro middleware or plugins to populate context:

ts
// server/middleware/context.ts
export default defineEventHandler(async (event) => {
  // Add database to context
  event.context.db = await useDatabase()

  // Add auth to context
  const token = getHeader(event, 'authorization')
  if (token) {
    event.context.auth = await verifyToken(token)
  }

  // Add services
  event.context.emailService = useEmailService()
  event.context.storageService = useStorageService()
})

Use Custom Context

Now access your custom context in resolvers:

ts
import type { User } from '#graphql/server'

export const userQueries = defineQuery({
  users: async (_, __, context) => {
    // Fully typed database access
    return await context.db.user.findMany()
  },

  me: async (_, __, context) => {
    const userId = context.auth?.userId

    if (!userId) {
      throw new GraphQLError('Unauthorized', {
        extensions: { code: 'UNAUTHENTICATED' }
      })
    }

    return await context.db.user.findUnique({
      where: { id: userId }
    })
  },
})

export const userMutations = defineMutation({
  createUser: async (_, { input }, context) => {
    const user = await context.db.user.create({
      data: input
    })

    // Use email service from context
    await context.emailService.sendWelcome(user.email)

    return user
  },
})

Common Context Patterns

Database Connection

ts
// server/graphql/context.ts
import type { PrismaClient } from '@prisma/client'

declare module 'h3' {
  interface H3EventContext {
    db: PrismaClient
  }
}
ts
// server/middleware/database.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export default defineEventHandler((event) => {
  event.context.db = prisma
})
ts
// Resolver
export const postQueries = defineQuery({
  posts: async (_, __, context) => {
    return await context.db.post.findMany()
  },
})

Authentication

ts
// server/graphql/context.ts
declare module 'h3' {
  interface H3EventContext {
    auth?: {
      userId: string
      email: string
      role: string
    }
  }
}
ts
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (token) {
    try {
      const payload = await verifyJWT(token)
      event.context.auth = {
        userId: payload.sub,
        email: payload.email,
        role: payload.role,
      }
    }
    catch (error) {
      // Invalid token - context.auth remains undefined
    }
  }
})
ts
// Resolver with auth check
export const userQueries = defineQuery({
  me: (_, __, context) => {
    if (!context.auth) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      })
    }

    return findUser(context.auth.userId)
  },
})

Request Information

ts
// server/graphql/context.ts
declare module 'h3' {
  interface H3EventContext {
    requestInfo: {
      ip: string
      userAgent: string
      referer?: string
    }
  }
}
ts
// server/middleware/request-info.ts
export default defineEventHandler((event) => {
  event.context.requestInfo = {
    ip: getRequestIP(event) || 'unknown',
    userAgent: getHeader(event, 'user-agent') || 'unknown',
    referer: getHeader(event, 'referer'),
  }
})

Caching

ts
// server/graphql/context.ts
import type { RedisClient } from 'redis'

declare module 'h3' {
  interface H3EventContext {
    cache: RedisClient
  }
}
ts
// server/middleware/cache.ts
import { createClient } from 'redis'

const redis = createClient()
await redis.connect()

export default defineEventHandler((event) => {
  event.context.cache = redis
})
ts
// Resolver with caching
export const postQueries = defineQuery({
  post: async (_, { id }, context) => {
    const cacheKey = `post:${id}`

    // Try cache first
    const cached = await context.cache.get(cacheKey)
    if (cached) {
      return JSON.parse(cached)
    }

    // Fetch from database
    const post = await context.db.post.findUnique({ where: { id } })

    // Cache for 5 minutes
    await context.cache.setEx(cacheKey, 300, JSON.stringify(post))

    return post
  },
})

Context in GraphQL Config

You can also enhance context in the GraphQL config:

ts
// server/graphql/config.ts
export default defineGraphQLConfig({
  context: async ({ event }) => {
    // Add extra context properties specific to GraphQL
    return {
      // event.context already has middleware-added properties
      startTime: Date.now(),
      requestId: crypto.randomUUID(),
    }
  },
})

Access merged context:

ts
export const userQueries = defineQuery({
  users: async (_, __, context) => {
    console.log(context.requestId) // From GraphQL config
    console.log(context.db) // From middleware
    console.log(context.auth) // From middleware
  },
})

Context Best Practices

1. Initialize in Middleware

ts
// ✅ Good - Initialize once in middleware
export default defineEventHandler((event) => {
  event.context.db = globalPrisma
})

// ❌ Bad - Create new instance per resolver
export const userQueries = defineQuery({
  users: async () => {
    const db = new PrismaClient()
    return await db.user.findMany()
  },
})

2. Type Everything

ts
// ✅ Good - Fully typed
declare module 'h3' {
  interface H3EventContext {
    db: PrismaClient
    auth?: AuthPayload
  }
}

// ❌ Bad - Untyped context
declare module 'h3' {
  interface H3EventContext {
    db: any
    auth: any
  }
}

3. Optional vs Required

ts
// ✅ Good - Optional auth
declare module 'h3' {
  interface H3EventContext {
    auth?: { // Optional - not all routes need auth
      userId: string
    }
  }
}

// ✅ Good - Required database
declare module 'h3' {
  interface H3EventContext {
    db: Database // Required - always available
  }
}

4. Validate in Resolvers

ts
// ✅ Good - Check context in resolver
export const userQueries = defineQuery({
  me: (_, __, context) => {
    if (!context.auth) {
      throw new GraphQLError('Unauthorized')
    }
    return findUser(context.auth.userId)
  },
})

// ❌ Bad - Assume context exists
export const userQueries = defineQuery({
  me: (_, __, context) => {
    // This will crash if auth is undefined
    return findUser(context.auth.userId)
  },
})

5. Separate Concerns

ts
// ✅ Good - Separate middleware files
// server/middleware/01.database.ts
// server/middleware/02.auth.ts
// server/middleware/03.services.ts

// ❌ Bad - Everything in one file
// server/middleware/context.ts (500 lines)

Real-World Example

Complete authentication and database example:

ts
// server/graphql/context.ts
import type { PrismaClient } from '@prisma/client'

declare module 'h3' {
  interface H3EventContext {
    db: PrismaClient
    auth?: {
      userId: string
      email: string
      role: 'ADMIN' | 'USER'
    }
  }
}
ts
// server/middleware/01.database.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export default defineEventHandler((event) => {
  event.context.db = prisma
})
ts
// server/middleware/02.auth.ts
import { verify } from 'jsonwebtoken'

export default defineEventHandler(async (event) => {
  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (token) {
    try {
      const payload = verify(token, process.env.JWT_SECRET!) as any
      event.context.auth = {
        userId: payload.userId,
        email: payload.email,
        role: payload.role,
      }
    }
    catch {
      // Invalid token
    }
  }
})
ts
// server/graphql/users.resolver.ts
export const userQueries = defineQuery({
  // Public query
  users: async (_, __, context) => {
    return await context.db.user.findMany()
  },

  // Protected query
  me: async (_, __, context) => {
    if (!context.auth) {
      throw new GraphQLError('Not authenticated', {
        extensions: { code: 'UNAUTHENTICATED' }
      })
    }

    return await context.db.user.findUnique({
      where: { id: context.auth.userId }
    })
  },

  // Admin-only query
  adminStats: async (_, __, context) => {
    if (!context.auth || context.auth.role !== 'ADMIN') {
      throw new GraphQLError('Not authorized', {
        extensions: { code: 'FORBIDDEN' }
      })
    }

    return await context.db.user.count()
  },
})

Debugging Context

Log context to see what's available:

ts
export const debugQuery = defineQuery({
  debug: (_, __, context) => {
    console.log('Context keys:', Object.keys(context))
    console.log('Auth:', context.auth)
    console.log('Database:', !!context.db)
    return 'Check console'
  },
})

Next Steps

🔐 Custom Directives

Use context in custom directives

Directives Guide →

🔧 Resolvers

Use context in resolvers

Resolvers Guide →

⚡ Performance

Optimize context usage

Performance →

🧪 Testing

Mock context in tests

Testing →

Released under the MIT License.