GraphQL API with Apollo Server
Schema, resolvers, context, dataloaders, and error handling
What You’ll Build
After following this guide, you will have a working implementation of graphql api with apollo server in your project. Build a GraphQL API with Apollo Server. Covers type definitions, resolvers, authentication context, N+1 query prevention with DataLoader, and custom error handling. GraphQL gives clients the power to request exactly the data they need.
Use Cases & Problems Solved
- Build reliable server endpoints that clients can consume consistently
- Handle common backend patterns like routing, middleware, and error handling
- Provide a clean interface between your frontend and data layer
Prerequisites
- Node.js 18+
- Understanding of GraphQL concepts
Step-by-Step Implementation
Install Apollo Server
The following snippet shows how to install apollo server. Copy this into your project and adjust the values for your environment.
npm install @apollo/server graphql dataloader
Define schema and resolvers
The following snippet shows how to define schema and resolvers. Copy this into your project and adjust the values for your environment.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `#graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
const resolvers = {
Query: {
users: (_, __, { db }) => db.user.findMany(),
user: (_, { id }, { db }) => db.user.findUnique({ where: { id } }),
},
Mutation: {
createUser: (_, args, { db }) => db.user.create({ data: args }),
},
User: {
posts: (parent, _, { loaders }) => loaders.postsByUser.load(parent.id),
},
};
DataLoader to prevent N+1 queries
The following snippet shows how to dataloader to prevent n+1 queries. Copy this into your project and adjust the values for your environment.
import DataLoader from 'dataloader';
function createLoaders(db) {
return {
postsByUser: new DataLoader(async (userIds) => {
const posts = await db.post.findMany({ where: { authorId: { in: userIds } } });
return userIds.map(id => posts.filter(p => p.authorId === id));
}),
};
}
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
db: prisma,
loaders: createLoaders(prisma),
user: await getUserFromToken(req.headers.authorization),
}),
});
⚠️ Don’t Do This
❌ Resolving nested relations without DataLoader (N+1 problem)
// For 100 users, this makes 101 database queries!
User: {
posts: async (parent) => {
return await db.post.findMany({ where: { authorId: parent.id } });
}
}
✅ Use DataLoader to batch and cache nested queries
// Batches all 100 user IDs into ONE query!
User: {
posts: (parent, _, { loaders }) => loaders.postsByUser.load(parent.id)
}
Testing
Add these tests to verify your API endpoints work correctly:
// __tests__/api.test.ts
import { describe, it, expect } from 'vitest';
describe('GraphQL API with Apollo Server', () => {
it('should return 200 for valid requests', async () => {
const res = await fetch('/api/endpoint', { method: 'GET' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toBeDefined();
});
it('should return 400 for invalid input', async () => {
const res = await fetch('/api/endpoint', {
method: 'POST',
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
it('should handle errors gracefully', async () => {
const res = await fetch('/api/endpoint/nonexistent');
expect(res.status).toBe(404);
});
});
Verification
# Start server, then open http://localhost:4000
# Run query in Apollo Sandbox:
query {
users {
id
name
posts { title }
}
} Related Specs
Secure Webhook Handler
Signature verification, idempotency, retry handling, and queue processing
Production-Ready REST API (Express)
Error handling, Zod validation, rate limiting, CORS, and structured logging
Real-Time Updates with SSE
EventSource API, reconnection, broadcasting, and Express/Fastify SSE