GraphQL is a query language for APIs that lets clients request exactly the data they need. Unlike REST with fixed endpoints, GraphQL exposes a single endpoint with a typed schema. It solves over-fetching, under-fetching, and the N+1 endpoint problem. Here’s how to build one from scratch.
Core Concepts
A GraphQL API is defined by its schema — a strongly typed contract. Queries read data, Mutations write data, Subscriptions stream real-time updates. Each field has a resolver — a function returning the data. The execution engine calls resolvers in parallel and assembles the response.
Building with Apollo Server
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const { GraphQLError } = require('graphql');
const typeDefs = `#graphql
type User { id: ID!; name: String!; email: String!; posts: [Post!]!; postCount: Int! }
type Post { id: ID!; title: String!; body: String!; published: Boolean!; author: User! }
input CreateUserInput { name: String!; email: String! }
input CreatePostInput { title: String!; body: String!; authorId: ID!; published: Boolean = false }
type Query { users: [User!]!; user(id: ID!): User; posts(published: Boolean): [Post!]! }
type Mutation { createUser(input: CreateUserInput!): User!; createPost(input: CreatePostInput!): Post!; publishPost(id: ID!): Post! }
`;
let users = [
{ id: '1', name: 'Alice Chen', email: 'alice@example.com' },
{ id: '2', name: 'Bob Martinez', email: 'bob@example.com' },
];
let posts = [
{ id: '1', title: 'GraphQL Basics', body: 'An intro...', published: true, authorId: '1' },
{ id: '2', title: 'Advanced Queries', body: 'Deep dive...', published: true, authorId: '1' },
];
let nextId = { user: 3, post: 3 };
const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(u => u.id === id) || (() => { throw new GraphQLError('Not found'); })(),
posts: (_, { published }) => published !== undefined ? posts.filter(p => p.published === published) : posts,
},
Mutation: {
createUser: (_, { input }) => {
if (users.some(u => u.email === input.email)) throw new GraphQLError('Email exists');
const user = { id: String(nextId.user++), ...input };
users.push(user);
return user;
},
createPost: (_, { input }) => {
const post = { id: String(nextId.post++), ...input };
posts.push(post);
return post;
},
publishPost: (_, { id }) => { const p = posts.find(p => p.id === id); p.published = true; return p; },
},
User: {
posts: (user) => posts.filter(p => p.authorId === user.id),
postCount: (user) => posts.filter(p => p.authorId === user.id).length,
},
Post: { author: (post) => users.find(u => u.id === post.authorId) },
};
(async () => {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`GraphQL API at ${url}`);
})();
Querying the API
# Single request replaces 2+ REST calls
query { users { name postCount posts { title published } } }
# Precise data fetching — only the fields you need
query { user(id: "1") { name email posts { title body } } }
# Mutations
mutation { createUser(input: { name: "Charlie", email: "charlie@example.com" }) { id name } }
mutation { createPost(input: { title: "New Post", body: "Content...", authorId: "1", published: true }) { id title author { name } } }
N+1 Problem & DataLoader
50 posts each resolving author = 51 queries. DataLoader batches and caches: collects all requested IDs, makes one batched query, distributes results.
GraphQL vs REST
Choose GraphQL for multiple client types with different data needs, complex nested relationships, or rapidly evolving frontends. Stick with REST for simple CRUD, file uploads, caching-heavy workloads, or teams without GraphQL experience. They can coexist — REST for simple resources, GraphQL for complex aggregation.
Further reading: GraphQL Docs | Apollo Server Docs

Leave a Reply