GraphQL Fundamentals: Complete Beginner's Guide to Schema, Queries & Operations
Master the core building blocks of GraphQL in this comprehensive guide. From understanding GraphQL's architecture and Schema Definition Language (SDL) to writing your first queries, mutations, and subscriptions. This tutorial covers everything beginners need to start building modern APIs.
Core Concepts
GraphQL Overview and History
GraphQL is a query language for APIs and runtime for executing queries, developed internally by Facebook in 2012 to solve mobile app data fetching issues, then open-sourced in 2015. It allows clients to request exactly the data they need in a single request, eliminating over-fetching and under-fetching problems common in traditional APIs.
GraphQL vs REST Comparison
┌─────────────────────────────────────────────────────────────────┐ │ REST vs GraphQL │ ├─────────────────────────────┬───────────────────────────────────┤ │ REST │ GraphQL │ ├─────────────────────────────┼───────────────────────────────────┤ │ Multiple endpoints │ Single endpoint (/graphql) │ │ GET /users, GET /posts │ POST /graphql │ │ Over/under-fetching │ Exact data fetching │ │ Versioning (v1, v2) │ Schema evolution │ │ Multiple round trips │ Single request │ │ Server-driven response │ Client-driven response │ └─────────────────────────────┴───────────────────────────────────┘
GraphQL Architecture and Execution Model
GraphQL operates through a layered execution model where a query is parsed, validated against the schema, and then executed by resolvers that traverse the query tree, with each field resolved independently and results assembled into the final JSON response matching the query shape.
┌──────────┐ ┌───────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ Query ───▶ Parse ───▶ Validate ───▶ Execute ───▶ Response └──────────┘ └───────┘ └──────────┘ │ Resolvers │ └──────────┘ └───────────┘
GraphQL Specification
The GraphQL specification is a detailed document maintained by the GraphQL Foundation that defines the language syntax, type system, introspection, validation rules, and execution semantics, ensuring consistent implementation across all GraphQL servers regardless of programming language.
GraphiQL and GraphQL Playground
GraphiQL and GraphQL Playground are interactive, in-browser IDEs for exploring and testing GraphQL APIs, featuring auto-completion, syntax highlighting, documentation explorer, query history, and real-time error feedback. Playground is now deprecated in favor of the newer GraphiQL 2.0.
┌────────────────────────────────────────────────────────────┐ │ GraphiQL [▶ Run] │ ├─────────────────────────────┬──────────────────────────────┤ │ query { │ { │ │ user(id: "1") { │ "data": { │ │ name │ "user": { │ │ email │ "name": "John", │ │ } │ "email": "j@test.com" │ │ } │ } │ │ │ } │ ├─────────────────────────────┤ } │ │ DOCS │ SCHEMA │ │ └─────────────────────────────┴──────────────────────────────┘
Introspection
Introspection is a built-in GraphQL feature that allows clients to query the schema itself using special meta-fields like __schema, __type, and __typename, enabling tools to auto-generate documentation, provide autocomplete, and validate queries at development time.
# Query the schema for all types { __schema { types { name kind } } __type(name: "User") { fields { name type { name } } } }
Schema Definition Language (SDL)
Schema Basics
The schema is the contract between client and server, defining all types, queries, mutations, and subscriptions available in your API through the SDL, with Query and Mutation being special root types that serve as entry points.
schema { query: Query mutation: Mutation subscription: Subscription } type Query { users: [User!]! }
Scalar Types (Int, Float, String, Boolean, ID)
Scalars are primitive leaf values in GraphQL that resolve to concrete data:
Int(32-bit signed integer)Float(double-precision floating-point)String(UTF-8 text)Boolean(true/false)ID(unique identifier serialized as String).
type Product { id: ID! # Unique identifier name: String! # Text price: Float! # Decimal number quantity: Int! # Whole number inStock: Boolean! # true/false }
Object Types
Object types are the core building blocks of a GraphQL schema, representing entities with named fields that can return scalars, other objects, or lists, forming the nodes in your data graph.
type User { id: ID! name: String! email: String! posts: [Post!]! # Relationship to another object type profile: Profile }
Input Types
Input types are special types used exclusively for arguments in mutations and queries, allowing you to pass complex structured data as parameters. They cannot have fields that return other object types or contain cycles.
input CreateUserInput { name: String! email: String! age: Int } type Mutation { createUser(input: CreateUserInput!): User! }
Enum Types
Enums define a type restricted to a specific set of allowed values, providing type safety and self-documentation for fields that should only accept predefined options.
enum Status { DRAFT PUBLISHED ARCHIVED } enum Role { ADMIN USER GUEST } type Post { status: Status! }
Union Types
Unions represent a type that could be one of several object types, useful when a field can return completely different types that don't share common fields, resolved using inline fragments.
union SearchResult = User | Post | Comment type Query { search(term: String!): [SearchResult!]! } # Client query: # { search(term: "hello") { ... on User { name } ... on Post { title } } }
Interface Types
Interfaces define a set of fields that multiple object types must implement, enabling polymorphism and ensuring consistent field structure across related types while allowing additional type-specific fields.
interface Node { id: ID! createdAt: DateTime! } type User implements Node { id: ID! createdAt: DateTime! name: String! # Additional field } type Post implements Node { id: ID! createdAt: DateTime! title: String! # Additional field }
Non-nullable Fields
The exclamation mark ! modifier indicates a field will never return null, shifting null-handling responsibility to the server and providing stronger type guarantees to clients.
type User { id: ID! # Never null - guaranteed name: String! # Never null - guaranteed bio: String # Nullable - might be null email: String! # Never null - guaranteed }
Lists and Arrays
Lists are denoted with square brackets and can be combined with non-null modifiers at both the list and item level, creating four distinct nullability patterns.
type Query { tags: [String] # List nullable, items nullable users: [User!] # List nullable, items non-null posts: [Post]! # List non-null, items nullable comments: [Comment!]!# List non-null, items non-null (most common) }
Custom Scalars
Custom scalars extend the type system for domain-specific values like dates, URLs, or JSON, requiring both schema definition and server-side parsing/serialization logic.
scalar DateTime scalar URL scalar JSON scalar Email type Event { id: ID! date: DateTime! website: URL metadata: JSON contact: Email! }
Schema Comments and Descriptions
Descriptions use triple-quoted strings or regular strings above definitions, automatically appearing in introspection results and documentation tools like GraphiQL.
""" Represents a user in the system. Users can create posts and comments. """ type User { "The unique identifier" id: ID! "User's display name (2-50 characters)" name: String! "Email address - must be verified" email: String! }
Operations
Resolvers
Resolvers are functions that populate data for each field in your schema, receiving four arguments:
parent(result from parent resolver)args(field arguments)context(shared data like auth)info(query AST details).
const resolvers = { Query: { user: (parent, args, context, info) => { return context.db.users.findById(args.id); } }, User: { posts: (parent, args, context) => { return context.db.posts.findByUserId(parent.id); } } };
Queries
Queries are read-only operations that fetch data, executed in parallel at the same level, and always start from fields defined on the root Query type.
# Schema type Query { user(id: ID!): User users(limit: Int): [User!]! } # Client query query GetUser { user(id: "123") { name email posts { title } } }
Mutations
Mutations are write operations for creating, updating, or deleting data, executed sequentially (in order) to ensure predictable side effects, and should return the affected data.
type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean! } # Client mutation mutation CreateUser { createUser(input: { name: "John", email: "j@test.com" }) { id name } }
Subscriptions
Subscriptions enable real-time data updates via WebSocket connections, where the server pushes data to clients when specific events occur, commonly used for chat, notifications, or live feeds.
type Subscription { messageAdded(channelId: ID!): Message! userStatusChanged: User! } # Client subscription subscription OnNewMessage { messageAdded(channelId: "general") { id content author { name } } }
Arguments
Arguments allow passing parameters to fields for filtering, pagination, or customization, defined in the schema with types and optional default values.
type Query { users( limit: Int = 10 offset: Int = 0 status: Status sortBy: String = "createdAt" ): [User!]! post(id: ID!, includeComments: Boolean = false): Post }
Variables
Variables externalize dynamic values from queries, enabling query reuse, better caching, and safer handling of user input by separating the static query structure from dynamic data.
# Query with variable definitions query GetUser($userId: ID!, $includePosts: Boolean = false) { user(id: $userId) { name posts @include(if: $includePosts) { title } } } # Variables (sent as JSON): # { "userId": "123", "includePosts": true }
Aliases
Aliases let you rename field results in the response, essential when querying the same field multiple times with different arguments in a single request.
query GetMultipleUsers { admin: user(id: "1") { name role } guest: user(id: "2") { name role } } # Response: { "admin": { "name": "...", "role": "..." }, "guest": { ... } }
Fragments
Fragments are reusable units of fields that reduce query duplication and improve maintainability, defined with fragment keyword and spread into queries using ...FragmentName.
fragment UserFields on User { id name email avatar } query GetUsers { currentUser { ...UserFields } users { ...UserFields } admin: user(id: "1") { ...UserFields role } }
Inline Fragments
Inline fragments are anonymous fragments used for conditional field selection based on type, essential when dealing with unions, interfaces, or adding directives to field groups.
query Search { search(term: "graphql") { ... on User { name email } ... on Post { title content } ... on Comment { body } } }
Directives (@skip, @include, @deprecated)
Directives modify query execution or schema behavior: @skip(if: Boolean) omits a field, @include(if: Boolean) conditionally includes it, and @deprecated(reason: String) marks schema fields as deprecated.
# Schema directive type User { id: ID! name: String! username: String @deprecated(reason: "Use 'name' instead") } # Query directives query GetUser($withPosts: Boolean!, $skipEmail: Boolean!) { user(id: "1") { name email @skip(if: $skipEmail) posts @include(if: $withPosts) { title } } }
Operation Naming
Named operations improve debugging, logging, server-side analytics, and are required when a document contains multiple operations, use descriptive PascalCase names indicating the operation's purpose.
# Named query query GetUserProfile { user(id: "1") { name } } # Named mutation mutation CreateNewPost { createPost(input: { title: "Hello" }) { id } } # Anonymous (avoid in production) { user(id: "1") { name } }