Skip to content

GraphQL - Schema

The GraphQL Schema is the absolute heart of any GraphQL API. It serves as a strong contract between the client and the server, defining precisely what data clients can request, what mutations they can perform, and the structure of the data involved. It’s defined using the GraphQL Schema Definition Language (SDL).

Think of the schema as the API’s blueprint or documentation, but unlike traditional documentation, it’s machine-readable and enforced by the GraphQL server.

  • Types: Define the different kinds of objects, scalars, enums, interfaces, and unions that exist in your API.
  • Fields: Properties within types that hold data. Each field has a specific type.
  • Root Operation Types: Special object types that define the entry points for operations:
    • Query: Defines available read operations.
    • Mutation: Defines available write operations (create, update, delete).
    • Subscription: Defines available operations for real-time data updates.
  • Schema Definition (Optional but Recommended): Explicitly declares which types serve as the root operation types (query, mutation, subscription).

SDL provides a concise, human-readable syntax for defining schemas. It’s language-agnostic, meaning a schema defined in SDL can be implemented by a GraphQL server written in any language (JavaScript, Python, Java, C#, etc.).

Common SDL elements:

  • type: Defines an Object type.
  • scalar: Defines a custom primitive type (beyond built-in Int, Float, String, Boolean, ID).
  • enum: Defines a set of predefined string values.
  • input: Defines an Input Object type, used primarily for mutation arguments.
  • interface: Defines a contract that multiple Object types can implement.
  • union: Defines a type that can be one of several Object types.
  • ! (Exclamation Mark): Indicates a field or argument is Non-Nullable.
  • [] (Square Brackets): Indicates a field or argument is a List (array).
  • # or """: Used for comments or descriptions (descriptions become part of the introspectable schema).
schema.graphql
""" Represents a learning institution """
type College {
id: ID! # Unique identifier for the college
name: String!
location: String
rating: Float
students: [Student] # List of students associated with this college
}
""" Represents a student enrolled in a college """
type Student {
id: ID!
firstName: String!
lastName: String
email: String!
collegeId: ID! # Foreign key reference
college: College # The associated College object
}
# Defines the entry points for reading data
type Query {
""" Fetches a list of all students """
students: [Student]
""" Fetches a single student by their unique ID """
studentById(id: ID!): Student
""" Fetches a single college by its unique ID """
collegeById(id: ID!): College
}
# Input type for creating a new student
input CreateStudentInput {
firstName: String!
lastName: String
email: String!
collegeId: ID!
}
# Defines the entry points for modifying data
type Mutation {
""" Creates a new student """
createStudent(input: CreateStudentInput!): Student # Returns the created student
}
# Explicitly define the root operation types (good practice)
schema {
query: Query
mutation: Mutation
# subscription: Subscription # Add if you have subscriptions
}

You typically provide your schema definition (typeDefs) and corresponding resolvers to your Apollo Server instance during setup.

Method 1: Schema String in Code (Simple Cases)

Section titled “Method 1: Schema String in Code (Simple Cases)”

For very small schemas, you can define the SDL directly as a template string:

// server.js
import { ApolloServer } from '@apollo/server';
// ... other imports
const typeDefs = `#graphql
type Query {
greeting: String
}
`;
const resolvers = {
Query: { greeting: () => 'Hello!' },
};
const server = new ApolloServer({
typeDefs, // Pass schema string directly
resolvers,
});
// ... start server
Section titled “Method 2: Loading from .graphql Files (Recommended)”

For maintainability, it’s best to keep your schema in separate .graphql files.

  1. Create schema.graphql file with your SDL content (like the example above).

  2. Load the file content in your server setup:

// server.js
import { ApolloServer } from '@apollo/server';
import { readFileSync } from 'fs'; // Node.js file system module
import path from 'path'; // Node.js path module
import { fileURLToPath } from 'url'; // To handle ES module paths
// ... other imports
// Load resolvers (assuming resolvers.js exists)
import resolvers from './resolvers.js';
// Get the directory name in ES Modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load schema from file
const typeDefs = readFileSync(path.join(__dirname, 'schema.graphql'), {
encoding: 'utf-8',
});
const server = new ApolloServer({
typeDefs, // Pass loaded schema string
resolvers,
});
// ... start server

(Note: File loading might differ slightly if using CommonJS require instead of ES Modules import.)

A powerful feature enabled by the schema is introspection. GraphQL servers expose a special set of fields (prefixed with __, like __schema, __type) that allow clients to query the schema itself. Tools like Apollo Sandbox, GraphiQL, and Apollo Client DevTools use introspection to provide features like auto-completion, documentation browsing, and schema visualization without needing separate documentation endpoints.

Introspection is enabled by default in development environments and typically disabled in production for security reasons, although this is configurable.

The approach shown here, defining the schema using SDL first and then writing resolvers to match, is called schema-first development. It’s very popular due to the clarity and language-agnostic nature of SDL.

An alternative is code-first, where you define types and resolvers using programming language constructs (like classes or decorators in TypeScript with libraries like TypeGraphQL or NestJS GraphQL), and the SDL schema is generated automatically from your code. Both approaches are valid, and the choice often depends on team preference and project requirements.