StackPractices
beginner By Mathias Paulenko

Mock GraphQL Resolvers for Frontend Development

Set up mocked GraphQL resolvers with Apollo Server so frontend teams can develop against a fake API before the backend is ready.

Overview

Backend not ready? Frontend teams sit idle waiting for API contracts. Apollo Server’s built-in mocking fixes that — it auto-generates fake data for every field in the schema. UI devs get a working GraphQL endpoint in minutes, build against it, and swap auto-mocks for custom resolvers as the schema stabilizes.

I first used this pattern on a project where the backend team was six weeks behind on the GraphQL schema. Instead of waiting, we stood up an Apollo Server with mocks: true in about twenty minutes. The frontend team built the entire user list page against fake data, and when the real resolvers arrived, we swapped them in with preserveResolvers: true — no UI changes needed. That experience sold me on mocking as a workflow tool, not just a testing tool.

This recipe covers @apollo/server 4.x with @faker-js/faker 8.x for realistic fake data. The same concepts carry over to MSW 2.x when you’d rather intercept at the browser level.

When to Use

Reach for Apollo mocking when frontend and backend are being built in parallel, when you need a running GraphQL endpoint for a demo or prototype, or when you want to test UI components against realistic data shapes. It’s also handy for simulating error states — throw errors from specific resolvers and watch how the UI reacts before the real service exists. If you already have a real Apollo Server running, mocking layers on top without changing your schema.

When NOT to Use

Don’t use it when the backend is already available and you need real end-to-end contract validation; that calls for Integration Testing. For mocking HTTP at the browser level without a server, MSW fits better. And if the schema is still changing rapidly, mocks can hide breaking schema changes. For production error handling patterns, see GraphQL Error Handling Best Practices.

Solution

Enable built-in mocking

// mock-server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    role: String!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
    publishedAt: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
  }
`;

const server = new ApolloServer({
  typeDefs,
  mocks: true,
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});

console.log(`Mock server ready at ${url}`);

Apollo auto-generates values based on scalar types: random strings for String, incrementing numbers for Int and ID, and ISO timestamps for fields named like dates.

Customize mocks with scalars

import { ApolloServer } from '@apollo/server';

const mocks = {
  ID: () => crypto.randomUUID(),
  String: () => 'Lorem ipsum',
  Int: () => Math.floor(Math.random() * 1000),
  Boolean: () => Math.random() > 0.5,
};

const server = new ApolloServer({
  typeDefs,
  mocks,
});

Mock specific types and fields

const mocks = {
  User: () => ({
    id: () => crypto.randomUUID(),
    name: () => faker.person.fullName(),
    email: () => faker.internet.email(),
    role: () => faker.helpers.arrayElement(['admin', 'editor', 'viewer']),
  }),

  Post: () => ({
    id: () => crypto.randomUUID(),
    title: () => faker.lorem.sentence(),
    content: () => faker.lorem.paragraphs(3),
    publishedAt: () => faker.date.recent().toISOString(),
  }),

  Query: () => ({
    users: () => Array.from({ length: 5 }, () => ({})),
    posts: () => Array.from({ length: 10 }, () => ({})),
  }),
};

const server = new ApolloServer({
  typeDefs,
  mocks,
});

Toggle mocking by environment

const server = new ApolloServer({
  typeDefs,
  resolvers: process.env.NODE_ENV === 'production' ? realResolvers : undefined,
  mocks: process.env.MOCK_API === 'true',
});

// Or combine real resolvers with mock fallback
const server = new ApolloServer({
  typeDefs,
  resolvers: realResolvers,
  mocks: process.env.NODE_ENV === 'development'
    ? { mocks, preserveResolvers: true }
    : false,
});

With preserveResolvers: true, Apollo uses your real resolvers where they exist and falls back to mocks for unimplemented fields.

Explanation

flowchart diagram: GraphQL Schema

Apollo’s auto-mocking inspects the schema and generates a default value for each scalar, so strings, numbers, booleans, and lists are populated automatically. You can override those defaults with custom mock functions per type or per scalar. With a User mock, each field gets its own generator — so you decide exactly what name, email, and role spit out.

The preserveResolvers option is what makes incremental migrations painless. Fields with a resolver keep their real implementation; fields without one fall back to the mock. So you ship the backend piece by piece — the frontend never notices the difference.

Faker integration produces realistic names, emails, sentences, and dates, so the UI looks and behaves as if it were connected to real data.

One trade-off that bit me: mock data can give you false confidence. If your mock always returns a list of 10 items, you won’t catch the bug that triggers when the list is empty. That’s why I always mock edge cases explicitly — empty lists, null fields, error responses, and unusually long strings. It’s tempting to skip that, but the ten minutes you spend on edge-case mocks will save you hours of debugging down the road.

Another gotcha: @apollo/server 4.x changed the mocking API from Apollo Server 3.x. In 3.x, you passed mocks to the ApolloServer constructor with mockEntireSchema: false to preserve resolvers. In 4.x, the option is preserveResolvers: true inside the mocks object. If you’re migrating, double-check your mock configuration — the error messages aren’t always obvious when the API doesn’t match.

Variants

Custom scalars and enums

When your schema uses custom scalars or enums, provide mock functions for them explicitly:

import { ApolloServer } from '@apollo/server';

const typeDefs = gql`
  enum Role {
    ADMIN
    EDITOR
    VIEWER
  }

  scalar Date

  type User {
    id: ID!
    name: String!
    role: Role!
    createdAt: Date!
  }
`;

const mocks = {
  Date: () => new Date().toISOString(),
  Role: () => faker.helpers.arrayElement(['ADMIN', 'EDITOR', 'VIEWER']),
  User: () => ({
    name: () => faker.person.fullName(),
    role: () => faker.helpers.arrayElement(['ADMIN', 'EDITOR', 'VIEWER']),
    createdAt: () => faker.date.past().toISOString(),
  }),
};

const server = new ApolloServer({
  typeDefs,
  mocks,
});

Without custom scalar mocks, Apollo returns null for those fields, which can break the frontend if it expects a valid value.

Pagination with Relay connections

For schemas using Relay-style cursor pagination, mock the connection structure:

const mocks = {
  Query: () => ({
    users: () => {
      const edges = Array.from({ length: 10 }, (_, i) => ({
        node: {
          id: `user-${i}`,
          name: faker.person.fullName(),
          email: faker.internet.email(),
        },
        cursor: `cursor-${i}`,
      }));
      return {
        edges,
        pageInfo: {
          hasNextPage: true,
          hasPreviousPage: false,
          startCursor: 'cursor-0',
          endCursor: 'cursor-9',
        },
        totalCount: 100,
      };
    },
  }),
};

This lets the frontend test infinite scroll, load-more buttons, and cursor-based navigation without a real backend.

Mock with MSW

For frontend-only mocking without a running server, use MSW with a GraphQL handler:

import { graphql } from 'msw';

export const handlers = [
  graphql.query('GetUsers', (req, res, ctx) => {
    return res(
      ctx.data({
        users: Array.from({ length: 5 }, () => ({
          id: crypto.randomUUID(),
          name: faker.person.fullName(),
          email: faker.internet.email(),
        })),
      })
    );
  }),
];

Seeded mocks for reproducible tests

import { faker } from '@faker-js/faker';

faker.seed(12345);

const mocks = {
  User: () => ({
    name: () => faker.person.fullName(),
    email: () => faker.internet.email(),
  }),
};

With a fixed seed, every server start produces the same fake data — useful for snapshot tests.

Error mocking

Simulate error responses to test error handling in the UI:

const mocks = {
  Query: () => ({
    user: () => { throw new Error('User not found'); },
  }),
};

const server = new ApolloServer({
  typeDefs,
  mocks,
});

Best Practices

  • Use realistic data from faker so UI reviews catch layout and behavior issues.
  • Start with auto-mocks, then customize fields one by one as the schema stabilizes.
  • Use preserveResolvers during migration to keep real resolvers for the parts already built while mocking the rest.
  • Seed faker in tests so snapshot output stays deterministic.
  • Mock list lengths that match real-world cases — one item isn’t enough to test pagination or empty states.

Common Mistakes

  • Mocking with empty strings. The UI may hide or collapse empty values, hiding layout bugs.
  • Not mocking list lengths. A list mock returning one item doesn’t test pagination or empty states.
  • Forgetting to disable mocks in production. Use environment variables to toggle mocking.
  • Not testing error states. Mock error responses to verify the UI handles them.
  • Letting mocks drift from real resolvers. Keep mock shapes aligned with the production schema.

Summary

  • Start with mocks: true — Apollo auto-generates values for every scalar. Zero custom code required, and the endpoint just works.
  • Layer in faker for realistic data — empty strings and random UUIDs hide layout bugs. Use @faker-js/faker 8.x for names, emails, and dates that look real.
  • Use preserveResolvers: true during migration — real resolvers for built fields, mocks for the rest. Swap them in incrementally without breaking the frontend.
  • Toggle mocks by environment — never ship mocks to production. Use NODE_ENV or MOCK_API to control the flag.
  • Seed faker in tests — faker.seed(12345) makes snapshot output deterministic and avoids flaky CI runs.

Companion code: The graphql-mocking-apollo-server companion repo has runnable TypeScript examples for every variant shown here.

See Also

Frequently Asked Questions

Can I mock only part of the schema?

Yes. Use preserveResolvers: true and provide real resolvers for implemented fields. Apollo mocks only the fields without resolvers.

How do I mock authentication?

Mock the context to return a fake user, or bypass auth checks entirely in mock mode.

Should I use Apollo mocking or MSW?

Use Apollo mocking when you want a running server. Use MSW when you want client-side interception without a server.

Can I mock subscriptions?

Apollo's built-in mocking doesn't support subscriptions. Use a custom PubSub with fake events for subscription testing.

How do I mock custom scalars and enums?

Provide mock functions for each custom scalar and enum in the mocks object. For example, Date: () => new Date().toISOString() and Role: () => faker.helpers.arrayElement(['ADMIN', 'EDITOR', 'VIEWER']). Without these, Apollo returns null for custom scalar fields.

How do I share mocks between tests and the dev server?

Export the mocks object from a shared module and import it in both your test setup and your dev server configuration. That keeps fake data consistent across environments. Use faker.seed() in tests for deterministic output.