TrueSpec

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

Advanced

Secure Webhook Handler

Signature verification, idempotency, retry handling, and queue processing

API & Backend
Intermediate

Production-Ready REST API (Express)

Error handling, Zod validation, rate limiting, CORS, and structured logging

API & Backend
Intermediate

Real-Time Updates with SSE

EventSource API, reconnection, broadcasting, and Express/Fastify SSE

API & Backend