GraphQL lets clients request exactly the data they need in a single request — no over-fetching, no under-fetching. Combined with Apollo Server and TypeScript, it’s a powerful way to build flexible APIs. This guide builds a complete GraphQL API from scratch.
📋 Table of Contents
GraphQL vs REST — Quick Comparison
| Aspect | REST | GraphQL |
|---|---|---|
| Data fetching | Multiple endpoints | Single endpoint, precise queries |
| Over-fetching | Common | Eliminated |
| Versioning | URL versions (v1, v2) | Schema evolution |
| Best for | Simple CRUD, caching | Complex, nested, client-driven data |
Project Setup
mkdir graphql-api && cd graphql-api
npm init -y
npm install @apollo/server graphql
npm install -D typescript tsx @types/node
npx tsc --init
Define the Schema
// src/schema.ts
export const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
publishPost(id: ID!): Post!
}
`;
Write Resolvers
// src/resolvers.ts
import { db } from './db';
export const resolvers = {
Query: {
users: () => db.users.findAll(),
user: (_: any, { id }: { id: string }) => db.users.findById(id),
posts: () => db.posts.findAll(),
},
Mutation: {
createUser: (_: any, { name, email }: { name: string; email: string }) => {
return db.users.create({ name, email });
},
createPost: (_: any, args: { title: string; content: string; authorId: string }) => {
return db.posts.create({ ...args, published: false });
},
publishPost: (_: any, { id }: { id: string }) => {
return db.posts.update(id, { published: true });
},
},
// Field resolvers for relationships
User: {
posts: (parent: { id: string }) => db.posts.findByAuthor(parent.id),
},
Post: {
author: (parent: { authorId: string }) => db.users.findById(parent.authorId),
},
};
Create the Server
// src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
context: async ({ req }) => {
// Extract auth token, attach user to context
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return { user };
},
});
console.log("GraphQL ready at " + url);
Authentication in Resolvers
Mutation: {
createPost: (_: any, args, context) => {
// Require authentication
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return db.posts.create({ ...args, authorId: context.user.id });
},
},
Solving the N+1 Problem with DataLoader
Field resolvers can trigger N+1 queries (one query per parent). DataLoader batches and caches them:
npm install dataloader
import DataLoader from 'dataloader';
// Create a loader that batches user lookups
const userLoader = new DataLoader(async (ids: readonly string[]) => {
const users = await db.users.findByIds(ids);
// Return in the same order as requested ids
return ids.map(id => users.find(u => u.id === id));
});
// Use in resolver — batches all author lookups into one query
Post: {
author: (parent, _, context) => context.userLoader.load(parent.authorId),
},
Example Query
# Client requests exactly what it needs
query {
user(id: "1") {
name
posts {
title
published
}
}
}
# Response contains only requested fields:
# { "data": { "user": { "name": "Alice", "posts": [...] } } }
Frequently Asked Questions
Q: GraphQL or REST for my project?
A: GraphQL for complex, nested, client-driven data (mobile apps, dashboards with varied needs). REST for simple CRUD and when HTTP caching matters most. Many teams use both.
Q: How do I handle file uploads in GraphQL?
A: Use the graphql-upload package, or better, upload files to object storage (S3) via a separate REST endpoint or presigned URL and pass the URL in your GraphQL mutation.
Q: Is GraphQL harder to cache than REST?
A: Yes at the HTTP level (single endpoint, POST requests). Use Apollo Client’s normalized cache on the client and persisted queries or response caching on the server to compensate.
Q: How do I prevent expensive/malicious queries?
A: Add query depth limiting, complexity analysis, and rate limiting. Packages like graphql-depth-limit and graphql-query-complexity protect against deeply nested abusive queries.
Q: Should I use code-first or schema-first?
A: Schema-first (SDL strings, shown here) is clear and language-agnostic. Code-first (Nexus, TypeGraphQL) gives better TypeScript integration. Both work; pick based on team preference.
Conclusion
GraphQL with Apollo Server gives clients precise control over the data they fetch, eliminating over- and under-fetching. The setup here — typed schema, resolvers with relationships, context-based auth, and DataLoader for the N+1 problem — covers the essentials of a production GraphQL API. Add depth limiting and complexity analysis before going live, and pair with Apollo Client for a normalized cache on the frontend.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment