Your First Query
Learn how to create, test, and use GraphQL queries and mutations in your Nitro GraphQL application.
Understanding GraphQL Operations
GraphQL has three main operation types:
- Query: Read data (like GET in REST)
- Mutation: Modify data (like POST, PUT, DELETE in REST)
- Subscription: Real-time data streams (WebSocket-based)
In this guide, we'll focus on Queries and Mutations.
Creating a Query
Step 1: Define the Schema
First, define your query in a GraphQL schema file:
# server/graphql/posts/post.graphql
type Post {
id: ID!
title: String!
content: String!
authorId: ID!
publishedAt: DateTime!
}
extend type Query {
posts: [Post!]!
post(id: ID!): Post
}Using extend type
Use extend type Query to add fields to the existing Query type. This allows you to split your schema across multiple files.
Step 2: Implement the Resolver
Create a resolver to fetch the data:
// server/graphql/posts/post.resolver.ts
import type { Post } from '#graphql/server'
// Sample data
const posts: Post[] = [
{
id: '1',
title: 'Getting Started with GraphQL',
content: 'GraphQL is amazing...',
authorId: '1',
publishedAt: new Date('2024-01-01'),
},
{
id: '2',
title: 'Advanced GraphQL Patterns',
content: 'Learn about advanced techniques...',
authorId: '1',
publishedAt: new Date('2024-01-15'),
},
]
export const postQueries = defineQuery({
// Query: posts - Returns all posts
posts: () => {
return posts
},
// Query: post(id) - Returns a single post
post: (_, { id }) => {
return posts.find(p => p.id === id) || null
},
})Resolver Arguments
Resolver functions receive four arguments:
parent- The parent resolver's return valueargs- Arguments passed to the fieldcontext- Shared context object (H3Event)info- Field execution info (rarely used)
Step 3: Test in GraphQL Playground
Open http://localhost:3000/api/graphql and test:
query GetAllPosts {
posts {
id
title
content
publishedAt
}
}query GetSinglePost {
post(id: "1") {
id
title
content
}
}Creating a Mutation
Step 1: Define the Schema
Add mutation types to your schema:
# server/graphql/posts/post.graphql
input CreatePostInput {
title: String!
content: String!
authorId: ID!
}
input UpdatePostInput {
title: String
content: String
}
extend type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post
deletePost(id: ID!): Boolean!
}Step 2: Implement Mutation Resolvers
// server/graphql/posts/post.resolver.ts (continued)
export const postMutations = defineMutation({
createPost: async (_, { input }, context) => {
const newPost: Post = {
id: Date.now().toString(),
...input,
publishedAt: new Date(),
}
// Store using H3 storage
const posts = await context.storage?.getItem('posts') || []
posts.push(newPost)
await context.storage?.setItem('posts', posts)
return newPost
},
updatePost: async (_, { id, input }, context) => {
const posts = await context.storage?.getItem('posts') || []
const index = posts.findIndex(p => p.id === id)
if (index === -1) {
return null
}
// Merge updates
posts[index] = {
...posts[index],
...input,
}
await context.storage?.setItem('posts', posts)
return posts[index]
},
deletePost: async (_, { id }, context) => {
const posts = await context.storage?.getItem('posts') || []
const filtered = posts.filter(p => p.id !== id)
await context.storage?.setItem('posts', filtered)
// Return true if a post was deleted
return filtered.length < posts.length
},
})Step 3: Test Mutations
Create a post:
mutation CreateNewPost {
createPost(input: {
title: "My First Post"
content: "This is the content"
authorId: "1"
}) {
id
title
publishedAt
}
}Update a post:
mutation UpdateExistingPost {
updatePost(
id: "1"
input: {
title: "Updated Title"
}
) {
id
title
content
}
}Delete a post:
mutation DeletePost {
deletePost(id: "1")
}Using Context in Resolvers
The third argument to resolvers is the H3 Event context, which provides access to:
event- The raw H3 eventstorage- Nitro storage layer$fetch- Server-side fetch- Custom context properties you define
Example with authentication:
export const postQueries = defineQuery({
myPosts: async (_, __, context) => {
// Access user from context
const userId = context.auth?.userId
if (!userId) {
throw new Error('Unauthorized')
}
const posts = await context.storage?.getItem('posts') || []
return posts.filter(p => p.authorId === userId)
},
})Learn More About Context
See the Context Guide for detailed information on extending and using context.
Query Arguments and Variables
Optional Arguments
type Query {
posts(
limit: Int = 10
offset: Int = 0
authorId: ID
): [Post!]!
}export const postQueries = defineQuery({
posts: (_, { limit = 10, offset = 0, authorId }) => {
let results = posts
// Filter by author if provided
if (authorId) {
results = results.filter(p => p.authorId === authorId)
}
// Pagination
return results.slice(offset, offset + limit)
},
})Test with variables:
query GetPostsByAuthor($authorId: ID, $limit: Int) {
posts(authorId: $authorId, limit: $limit) {
id
title
}
}Variables:
{
"authorId": "1",
"limit": 5
}Async Resolvers
Most real-world resolvers are async (database calls, API requests):
export const postQueries = defineQuery({
posts: async (_, { limit = 10 }) => {
// Simulate database query
const db = await useDatabase()
const posts = await db.post.findMany({
take: limit,
orderBy: { publishedAt: 'desc' }
})
return posts
},
post: async (_, { id }) => {
const db = await useDatabase()
const post = await db.post.findUnique({
where: { id }
})
return post || null
},
})Error Handling
Throw errors to return GraphQL errors:
import { GraphQLError } from 'graphql'
export const postMutations = defineMutation({
deletePost: async (_, { id }, context) => {
const posts = await context.storage?.getItem('posts') || []
const post = posts.find(p => p.id === id)
if (!post) {
throw new GraphQLError('Post not found', {
extensions: {
code: 'NOT_FOUND',
id,
},
})
}
// Check authorization
if (post.authorId !== context.auth?.userId) {
throw new GraphQLError('Not authorized to delete this post', {
extensions: {
code: 'FORBIDDEN',
},
})
}
const filtered = posts.filter(p => p.id !== id)
await context.storage?.setItem('posts', filtered)
return true
},
})Response:
{
"errors": [
{
"message": "Post not found",
"extensions": {
"code": "NOT_FOUND",
"id": "999"
}
}
],
"data": null
}Error Handling Guide
See the Error Handling Guide for advanced patterns.
Field Resolvers
Resolve nested fields with field resolvers:
type Post {
id: ID!
title: String!
author: User! # ← Nested field
}
type User {
id: ID!
name: String!
}export const postTypes = defineType({
Post: {
// Resolve the author field
author: async (parent, _, context) => {
// parent.authorId comes from the Post resolver
const users = await context.storage?.getItem('users') || []
return users.find(u => u.id === parent.authorId)
},
},
})Now when you query:
query {
post(id: "1") {
id
title
author {
id
name
}
}
}The author field automatically resolves!
Combining Multiple Resolvers
You can split resolvers across multiple files:
// server/graphql/posts/queries.resolver.ts
export const postQueries = defineQuery({
posts: () => [...],
post: () => {...},
})// server/graphql/posts/mutations.resolver.ts
export const postMutations = defineMutation({
createPost: () => {...},
updatePost: () => {...},
deletePost: () => {...},
})// server/graphql/posts/types.resolver.ts
export const postTypes = defineType({
Post: {
author: () => {...},
},
})All three will be automatically discovered and merged.
Client-Side Usage (Nuxt)
After creating queries, use them in your Nuxt app:
# app/graphql/posts/get-posts.graphql
query GetPosts($limit: Int) {
posts(limit: $limit) {
id
title
content
publishedAt
}
}<script setup lang="ts">
const { GetPosts } = useGraphql()
const { data: posts } = await useAsyncData('posts', () =>
GetPosts({ limit: 10 })
)
</script>
<template>
<div v-for="post in posts?.posts" :key="post.id">
<h2>{{ post.title }}</h2>
<p>{{ post.content }}</p>
</div>
</template>