GraphQL Schemas
Learn how to define and organize GraphQL schemas in your Nitro GraphQL application.
What is a GraphQL Schema?
A GraphQL schema defines the structure of your API:
- Types: Data structures (User, Post, Product, etc.)
- Queries: Read operations
- Mutations: Write operations
- Inputs: Complex input arguments
- Enums: Fixed sets of values
- Scalars: Primitive types (String, Int, custom scalars)
Creating Your First Schema
Create a .graphql file in server/graphql/:
# server/graphql/schema.graphql
type Query {
hello: String!
}
type Mutation {
_empty: String
}Mutation Requirement
GraphQL Yoga requires at least one Mutation field. Use _empty: String if you don't have mutations yet.
Type Definitions
Object Types
Define custom types for your data:
type User {
id: ID!
name: String!
email: String!
age: Int
isActive: Boolean!
createdAt: DateTime!
}Field modifiers:
!- Non-nullable (required)[Type]- List/array[Type]!- Non-nullable list[Type!]!- Non-nullable list of non-nullable items
Examples:
type Post {
id: ID! # Required ID
title: String! # Required string
tags: [String!]! # Required array of required strings
comments: [Comment] # Optional array of optional comments
author: User! # Required User object
}Input Types
Use for complex mutation arguments:
input CreateUserInput {
name: String!
email: String!
age: Int
}
input UpdateUserInput {
name: String
email: String
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
}Input vs Type
- Type: Used for output (query/mutation results)
- Input: Used for input arguments
- They cannot be used interchangeably
Enums
Define fixed sets of values:
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type User {
id: ID!
name: String!
role: UserRole!
}Usage in resolvers:
export const userQueries = defineQuery({
usersByRole: (_, { role }) => {
// role is typed as 'ADMIN' | 'MODERATOR' | 'USER' | 'GUEST'
return users.filter(u => u.role === role)
},
})Scalars
Built-in scalars:
String- UTF-8 character sequenceInt- Signed 32-bit integerFloat- Signed double-precision floating-point valueBoolean- true or falseID- Unique identifier
Custom scalars (provided by graphql-scalars):
scalar DateTime
scalar JSON
scalar EmailAddress
scalar URL
type User {
id: ID!
email: EmailAddress!
website: URL
metadata: JSON
createdAt: DateTime!
}Nitro GraphQL automatically includes common scalars from the graphql-scalars library.
Extending Types
Split your schema across multiple files using extend:
# server/graphql/schema.graphql
type Query {
hello: String!
}
type Mutation {
_empty: String
}# server/graphql/users/user.graphql
type User {
id: ID!
name: String!
email: String!
}
extend type Query {
users: [User!]!
user(id: ID!): User
}
extend type Mutation {
createUser(input: CreateUserInput!): User!
}# server/graphql/posts/post.graphql
type Post {
id: ID!
title: String!
authorId: ID!
}
extend type Query {
posts: [Post!]!
post(id: ID!): Post
}
extend type Mutation {
createPost(input: CreatePostInput!): Post!
}All extensions are automatically merged into a single schema.
Relationships
One-to-One
type User {
id: ID!
name: String!
profile: UserProfile # One user has one profile
}
type UserProfile {
bio: String
avatar: String
userId: ID!
user: User! # Profile belongs to one user
}One-to-Many
type User {
id: ID!
name: String!
posts: [Post!]! # One user has many posts
}
type Post {
id: ID!
title: String!
authorId: ID!
author: User! # One post has one author
}Many-to-Many
type User {
id: ID!
name: String!
groups: [Group!]! # User belongs to many groups
}
type Group {
id: ID!
name: String!
members: [User!]! # Group has many users
}Resolver Implementation
See the Resolvers Guide for how to implement these relationships in your resolvers.
Interfaces
Define shared fields across types:
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String!
}
type Query {
node(id: ID!): Node
}Resolver:
export const nodeResolver = defineQuery({
node: (_, { id }) => {
// Return User or Post
return findById(id)
},
})
export const nodeTypes = defineType({
Node: {
__resolveType(obj) {
if ('email' in obj)
return 'User'
if ('content' in obj)
return 'Post'
return null
},
},
})Union Types
Return one of several types:
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}Resolver:
export const searchTypes = defineType({
SearchResult: {
__resolveType(obj) {
if ('email' in obj)
return 'User'
if ('content' in obj)
return 'Post'
if ('postId' in obj)
return 'Comment'
return null
},
},
})Client query:
query Search($query: String!) {
search(query: $query) {
__typename
... on User {
id
name
email
}
... on Post {
id
title
content
}
... on Comment {
id
text
}
}
}Directives in Schema
Use directives to add metadata and behavior:
type Query {
# Authentication required
profile: User @auth
# Admin-only
adminData: String @auth(requires: "ADMIN")
# Cached for 5 minutes
expensiveData: String @cache(ttl: 300)
# Deprecated field
oldEndpoint: String @deprecated(reason: "Use newEndpoint instead")
}Custom Directives
Learn how to create custom directives in the Custom Directives Guide.
File Organization
Single File Approach
Good for small projects:
server/graphql/
└── schema.graphqlFeature-Based Organization
Recommended for larger projects:
server/graphql/
├── schema.graphql # Root Query/Mutation
├── users/
│ └── user.graphql # User types, extend Query/Mutation
├── posts/
│ └── post.graphql # Post types, extend Query/Mutation
├── comments/
│ └── comment.graphql # Comment types
└── _scalars.graphql # Shared scalarsDomain-Driven Organization
For very large projects:
server/graphql/
├── schema.graphql
├── auth/
│ ├── user.graphql
│ └── session.graphql
├── content/
│ ├── post.graphql
│ ├── comment.graphql
│ └── media.graphql
└── commerce/
├── product.graphql
├── order.graphql
└── payment.graphqlSchema Documentation
Add descriptions to your schema:
"""
Represents a user in the system.
Users can create posts and comments.
"""
type User {
"Unique identifier for the user"
id: ID!
"User's full name"
name: String!
"User's email address (must be unique)"
email: String!
"""
User's role in the system.
Determines permissions and access levels.
"""
role: UserRole!
}
"""
Input for creating a new user
"""
input CreateUserInput {
"User's full name (2-100 characters)"
name: String!
"Valid email address"
email: String!
}These descriptions appear in:
- GraphQL Playground
- Generated documentation
- IDE autocomplete
Common Patterns
Pagination
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type UserEdge {
cursor: String!
node: User!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type Query {
users(
first: Int
after: String
last: Int
before: String
): UserConnection!
}Filtering
input UserFilter {
name: String
email: String
role: UserRole
isActive: Boolean
createdAfter: DateTime
createdBefore: DateTime
}
type Query {
users(
filter: UserFilter
limit: Int = 10
offset: Int = 0
): [User!]!
}Sorting
enum SortOrder {
ASC
DESC
}
input UserSort {
field: String!
order: SortOrder!
}
type Query {
users(
sort: UserSort
limit: Int = 10
): [User!]!
}Response Unions
type User {
id: ID!
name: String!
}
type ValidationError {
field: String!
message: String!
}
union CreateUserResult = User | ValidationError
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}Client usage:
mutation {
createUser(input: {name: "John", email: "invalid"}) {
__typename
... on User {
id
name
}
... on ValidationError {
field
message
}
}
}Auto-Generated Files
Nitro GraphQL generates these schema-related files:
server/graphql/schema.ts
Export of your merged schema (auto-generated if it doesn't exist):
// This file is auto-generated
export const typeDefs = `
type Query { ... }
type Mutation { ... }
type User { ... }
`Don't edit this file - it's regenerated on changes.
graphql.config.ts
GraphQL IDE configuration (auto-generated):
export default {
schema: './server/graphql/**/*.graphql',
documents: './app/graphql/**/*.graphql'
}Disabling Auto-Generation
You can disable auto-generated files:
// nitro.config.ts
export default defineNitroConfig({
graphql: {
framework: 'graphql-yoga',
scaffold: {
graphqlConfig: false, // Don't generate graphql.config.ts
serverSchema: false, // Don't generate schema.ts
}
}
})See File Generation Control for details.
Best Practices
1. Use Descriptive Names
# ✅ Good
type User {
id: ID!
fullName: String!
emailAddress: String!
}
# ❌ Bad
type User {
id: ID!
n: String!
e: String!
}2. Prefer Input Types
# ✅ Good
input CreateUserInput {
name: String!
email: String!
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
# ❌ Bad (too many arguments)
type Mutation {
createUser(
name: String!
email: String!
age: Int
address: String
phone: String
): User!
}3. Make Fields Non-Nullable When Possible
# ✅ Good - Clear requirements
type User {
id: ID!
name: String!
email: String!
bio: String # Nullable - actually optional
}
# ❌ Bad - Everything nullable
type User {
id: ID
name: String
email: String
}4. Use Enums for Fixed Values
# ✅ Good
enum Status {
ACTIVE
INACTIVE
PENDING
}
# ❌ Bad
type User {
status: String # Any string allowed
}5. Document Your Schema
"""
User account in the system
"""
type User {
"Unique user identifier"
id: ID!
"User's display name"
name: String!
}