GraphQL Backend API Development Guide: Schema, Resolvers and Queries
GraphQL is a query language and runtime for APIs. Unlike REST's many endpoints with fixed response shapes, GraphQL exposes an entire data graph through a single endpoint, and the client declares exactly which fields it needs. Based on the official GraphQL documentation, this guide covers schemas, resolvers, queries/mutations, and tooling.
Why GraphQL
The core value of GraphQL is on-demand data: clients receive only the fields they request, naturally avoiding the over-fetching and round-trip multiplication common in REST. Because the response shape mirrors the query shape, teams can predict results without deep server knowledge. Its strongly typed schema makes the frontend/backend contract clear and machine-validatable.
A simple comparison makes the difference concrete. To fetch a user's name, their latest three posts, and the comment count for each post, REST typically needs three requests — user detail, post list, and per-post comment aggregation — and each response carries fields the client never uses. GraphQL needs one query, the client declares the fields it wants, and the server assembles the answer in a single response. As the API evolves, teams adjust the schema instead of adding endpoints and maintaining versioned REST routes.
| Dimension | REST | GraphQL |
|---|---|---|
| Endpoints | Multiple, one per resource | Single /graphql |
| Response shape | Fixed by the server | Declared by the client |
| Multiple resources | Several requests or server aggregation | One query |
| Type contract | External spec such as OpenAPI | Built into the schema |
| Caching | Mature HTTP caching | Needs extra tooling |
Schemas and the Type System
Every GraphQL service is described by a schema — a collection of types that defines what can be queried. Schemas are written in SDL (Schema Definition Language) and include six kinds of named types: Object, Scalar, Enum, Interface, Union, and Input Object:
type Character {
name: String!
appearsIn: [Episode!]!
}
type Query {
hero(episode: Episode): Character
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
String!is a Non-Null type: the server promises a value for this field;[Episode!]!is a non-null list of non-null Episodes;Queryis the root operation type — the entry point for every query.
Queries and Mutations
A schema must support query operations and may also define mutation and subscription types. Queries read data:
query {
hero(episode: JEDI) {
name
friends { name }
}
}
Mutations write data, typically accepting a whole Input Object:
mutation {
createReview(episode: JEDI, review: { stars: 5, commentary: "Great!" }) {
stars
}
}
Subscriptions: realtime data
subscription is the third root operation in GraphQL, and it fits chat, notifications, live dashboards, and similar realtime scenarios. The server publishes events and clients subscribe to specific fields over a persistent connection such as WebSocket; when the data changes, the server pushes the update. Compared with REST polling, this cuts latency and eliminates most wasted requests.
Input objects and custom scalars
Mutations typically accept a whole block of arguments through an Input Object. The inline object in createReview above works, but real projects usually define the input type explicitly so it can be validated and reused:
input ReviewInput {
stars: Int!
commentary: String
favoriteColor: Color
}
scalar Color
type Mutation {
createReview(episode: Episode, review: ReviewInput): Review
}
scalar Color is an example of a custom scalar. The built-in scalars are only Int, Float, String, Boolean, and ID — for dates, timestamps, JSON, or other business fields you define a custom scalar and implement its serialization and validation yourself. Input types and custom scalars are where real projects most often stumble, and where schema design skill shows most clearly.
How Resolvers Work
The schema describes what exists; resolvers decide how to fetch it. Every field maps to a resolver function, and the GraphQL executor calls these resolvers according to the query structure, assembling the response layer by layer. Resolvers can hit a database, call a REST service, or reach a third-party API — which is why GraphQL often serves as an aggregation layer unifying several backends behind one API.
A runnable server example
Here is a minimal working Node.js + Apollo Server example: one Query, one Character type, and two resolvers. friends reads from an in-memory dictionary for now; in a real project this would be a database query or a call to a downstream service:
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type Character { id: ID!, name: String!, friends: [Character] }
type Query { hero: Character }
`;
const data = {
'1': { id: '1', name: 'Luke', friends: ['2'] },
'2': { id: '2', name: 'Leia', friends: ['1'] },
};
const resolvers = {
Query: { hero: () => data['1'] },
Character: { friends: (c) => c.friends.map((id) => data[id]) },
};
new ApolloServer({ typeDefs, resolvers }).listen(4000)
.then(({ url }) => console.log('GraphQL ready at', url));
Once it is running, send { hero { name friends { name } } } from GraphiQL and you will see the assembled result. Getting this working and then swapping the in-memory data for a real database and auth logic is the smoothest path into GraphQL.
Performance and security practices
The common GraphQL performance problems are N+1 queries and deeply nested queries. Use DataLoader to batch related data, cap query depth and complexity, and cache expensive fields. Authorization is usually enforced once in the resolver layer rather than at fixed endpoints, so every entry point gets the same permission boundary. Before shipping, run a Schema Registry check for breaking changes so risky moves like removing or renaming a field are blocked before merge.
Pagination, caching, and error handling
For list queries, prefer "Connection" style pagination: return edges/node plus pageInfo with hasNextPage and a cursor. Cursors are more stable than offset pagination when data changes frequently. For caching, layer a second cache such as Redis on top of DataLoader and cache the responses of hot fields; Apollo's @cacheControl directive sets per-field TTLs. For errors, GraphQL reports failures in the errors array — throw a custom error type for business failures instead of stuffing errors into data, so clients can parse them uniformly.
Tooling and Ecosystem
- GraphiQL / GraphQL Playground: browser-based interactive IDEs for live query debugging;
- Apollo: the most popular client + server (Apollo Server) stack, with caching and federation support;
- GraphQL.js: the official reference implementation;
- Schema governance tools: review, version, and validate schema changes.
Use Cases and Trade-offs
GraphQL fits best when you have many client shapes (web + app + IoT), widely varying field needs, or several backend services to aggregate. The cost is more complex server implementation and caching, so not every API needs GraphQL — for simple, stable consumers, REST remains more direct.
FAQ
Will GraphQL replace REST? No — each has its place, and many teams use both in the same service: public-facing endpoints stay REST, while internal or aggregation APIs use GraphQL. What about overly deep queries? Cap maximum depth and complexity weights, and estimate the cost of every query at the gateway. How do you fix N+1? DataLoader deduplicates and batches loads per request; combined with IN queries at the database, it removes most N+1 problems. Is it hard to pick up? The hard part is schema design, not the syntax — start with a small internal tool before rolling it out to public APIs.
16IDC perspective
When choosing, weigh REST and GraphQL together — we have a full REST vs GraphQL Comparison and Selection Guide; for API fundamentals, read Website API Integration Basics. If you are exploring GraphQL at the edge, see Cloudflare Workers Support for Inbound TCP and gRPC. More backend content lives in the Backend Integration category.
References: GraphQL official docs · Schema https://graphql.org/learn/schema/; Apollo Server getting started https://www.apollographql.com/docs/apollo-server/getting-started/; GraphQL official tooling https://graphql.org/code/