不喜欢广告? 无广告 今天

GraphQL Schemas Write, Format, and Understand SDL Without the Boilerplate

更新于

GraphQL schemas are self-documenting contracts between server and client. This guide walks you through Schema Definition Language (SDL) from scratch — types, scalars, queries, mutations, input types, enums, and interfaces — so you can read and write schemas confidently.

GraphQL Schemas: Write, Format, and Understand SDL Without the Boilerplate 1
广告 移除?

If you’ve worked with REST APIs and then opened a GraphQL schema for the first time, the syntax probably looked familiar but not quite parseable. That’s normal. Schema Definition Language (SDL) is concise, but it carries a lot of meaning per line. This article breaks it down so you can read and write schemas without reaching for the docs every five minutes.

What a GraphQL Schema Actually Is

A GraphQL schema is the contract between your API server and every client that calls it. It defines exactly what data exists, what operations are available, and what shape the responses take. Unlike REST — where you discover endpoints through documentation, trial, and error — GraphQL makes the contract explicit and machine-readable.

Every GraphQL API starts with a schema written in SDL. The server validates all queries against it at runtime. If a client asks for a field that doesn’t exist in the schema, the request is rejected before any resolver runs. This is the core value of GraphQL’s type system: the schema is the source of truth for your API’s surface area.

SDL Basics: Types, Queries, and Mutations

Every SDL file is built from type definitions. Here’s the smallest useful schema possible:

type Query {
  hello: String
}

Query is the entry point for read operations. Every schema must have one. The field hello returns a String — one of five built-in scalar types.

The five built-in scalars are:

  • 细绳 — UTF-8 text
  • Int — 32-bit signed integer
  • Float — double-precision floating point
  • Boolean — true or false
  • ID — a unique identifier, serialized as a string

Real schemas define custom object types. Here’s a more typical pattern:

type User {
  id: ID!
  name: String!
  email: String!
  role: UserRole!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  body: String
  author: User!
  publishedAt: DateTime
}

type Query {
  user(id: ID!): User
  posts: [Post!]!
}

There’s a lot happening in those 20 lines. Let’s unpack it.

Non-Null Fields and Lists

! after a type means the field is non-null — it will never return null. Without it, the field is nullable and the client must handle a missing value. This distinction matters at runtime: if a non-null field resolver throws or returns null, GraphQL will propagate the null up the tree, potentially nulling out parent fields too.

Lists use square brackets: [Post] is a nullable list of nullable posts. [Post!]! is a non-null list of non-null posts. That’s the version you usually want — a field that always returns an array (possibly empty), and each element in it is always a real object, not null.

The four combinations, spelled out:

  • [Post] — nullable list, nullable items (rarely useful)
  • [Post!] — nullable list, non-null items
  • [Post]! — non-null list, nullable items
  • [Post!]! — non-null list, non-null items (most common)

Mutations: Writing Data

Queries are read-only. Mutations handle writes. The Mutation type is structured the same way as Query, but by convention its fields have side effects:

type Mutation {
  createPost(input: CreatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

注意 CreatePostInput! — that’s an input type, which leads directly to the next concept.

Input Types vs Output Types

This is one of the most common points of confusion in GraphQL schemas. You cannot reuse an object type like Post as an argument to a mutation. SDL has two separate kinds of types for a reason:

  • Object types (type) — used in responses. Can contain resolvers and complex field logic.
  • Input types (input) — used in arguments. Plain data, no resolvers. Fields can only reference scalars or other input types.
input CreatePostInput {
  title: String!
  body: String
  authorId: ID!
}

The separation keeps argument validation clean and prevents circular reference issues that would arise if output types (which can reference each other in complex graphs) were used directly as inputs.

Custom Scalars

The five built-in scalars don’t cover everything. A date-time string, a URL, a JSON blob — these need custom scalars. You declare them like this:

scalar DateTime
scalar JSON
scalar URL

The scalar declaration tells SDL what name to use. The actual serialization, deserialization, and validation logic lives in the server implementation — not the schema file. Libraries like graphql-scalars provide ready-made implementations for common types so you don’t have to write them from scratch.

Use custom scalars when a String is technically correct but semantically wrong. A field typed as DateTime communicates intent; a field typed as String forces the client to guess the format.

Enums

Enums constrain a field to a fixed set of string values. They’re more reliable than raw strings because the schema enforces the allowed values at the type level:

enum UserRole {
  ADMIN
  EDITOR
  VIEWER
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

A field typed as UserRole will never return an unexpected string. A field typed as String could return anything. Use enums whenever the set of values is known, stable, and meaningful to clients — it makes schema introspection significantly more useful.

Interfaces and Unions

When a field can return different types of objects, GraphQL offers two mechanisms: interfaces and unions.

Interfaces define a shared set of fields. Types that implement the interface must include those fields:

interface Node {
  id: ID!
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

Unions group types without requiring shared fields. They’re useful when the possible types have nothing structurally in common:

union SearchResult = User | Post | Comment

type Query {
  search(query: String!): [SearchResult!]!
}

Clients use inline fragments to handle each type in a union or interface: ... on User { name }. If you’re returning polymorphic data, prefer interfaces when types share fields, and unions when they don’t.

Directives

Directives modify behavior at the field or type level. Two are built into every GraphQL implementation:

  • @deprecated(reason: "Use newField instead") — marks a field as deprecated in introspection
  • @skip(if: Boolean)@include(if: Boolean) — client-side conditionals in queries

You can also define custom directives for things like authentication guards, rate limiting hints, or caching annotations. They’re declared in the schema and implemented on the server:

directive @auth(requires: UserRole = ADMIN) on FIELD_DEFINITION

type Mutation {
  deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
}

GraphQL vs REST: When to Use Which

GraphQL’s schema-first approach pays off in specific situations. It’s the right call when:

  • Multiple clients (web, mobile, third-party) need different subsets of the same data
  • You want to evolve the API without versioning — add fields freely, deprecate rather than remove
  • The domain is naturally graph-shaped with complex relationships between entities
  • You want a self-documenting API that clients can explore via introspection

REST is still the better option when:

  • You’re building simple CRUD endpoints with predictable, flat payloads
  • HTTP caching is critical — GraphQL POST requests don’t cache out of the box
  • The team is small and the API surface is stable — REST’s overhead is lower
  • You’re hitting the N+1 problem and don’t want to wire up DataLoader to fix it

The N+1 problem is worth pausing on. In a GraphQL resolver that fetches posts and each post’s author, a naive implementation fires one database query per post to load the author. With 50 posts, that’s 51 queries for one request. REST doesn’t have this problem because you control exactly what SQL runs per endpoint. In GraphQL, you fix it with batching (DataLoader or equivalent) — it’s solvable, but it’s one more thing to wire up correctly.

Format and Validate Your SDL

Schema files accumulate inconsistent indentation, mismatched spacing, and structural drift as teams grow. Before committing schema changes, run them through a formatter to normalize spacing, sort fields, and catch syntax errors before they hit CI.

GraphQL Schema Formatter on iotools.cloud does exactly this — paste your SDL, get back clean, consistently formatted output with validation errors surfaced inline. It handles multi-type schemas, custom scalars, directives, and the edge cases that trip up manual formatting. Useful as a final pass before committing or sharing a schema with another team.

想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?