Part 7: Testing, Performance, and Deployment

Shipping GraphQL with Confidence

A GraphQL service that is well-tested and performance-profiled behaves predictably under production load. This part covers the full quality lifecycle: unit testing resolvers, integration testing with a real database, measuring and fixing N+1 regressions, adding query complexity limits, and deploying the federated supergraph.

Testing the TypeScript Subgraph

Unit Testing Resolvers with executeOperation

Apollo Server v4 ships a executeOperation method for testing without starting a real HTTP server. I use this for all resolver unit tests.

npm install --save-dev jest @types/jest ts-jest
// jest.config.ts
export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/*.test.ts'],
};
// src/__tests__/products.test.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { typeDefs } from '../schema';
import { resolvers } from '../resolvers';
import { AppContext } from '../context';

// ─── Minimal fake context ───────────────────────────────────────────────────

function buildTestContext(overrides: Partial<AppContext> = {}): AppContext {
  const prismaMock = {
    product: {
      findUnique: jest.fn(),
      findMany: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
      count: jest.fn(),
    },
    category: {
      findUnique: jest.fn(),
      findMany: jest.fn(),
    },
  };

  return {
    prisma: prismaMock as any,
    loaders: {
      category: { load: jest.fn() } as any,
      productVariants: { load: jest.fn() } as any,
    },
    user: { id: 'user-1', email: '[email protected]', role: 'ADMIN' },
    ...overrides,
  };
}

// ─── Test Suite ─────────────────────────────────────────────────────────────

describe('Product resolvers', () => {
  let server: ApolloServer<AppContext>;

  beforeAll(async () => {
    server = new ApolloServer({
      schema: buildSubgraphSchema({ typeDefs, resolvers }),
    });
    await server.start();
  });

  afterAll(async () => {
    await server.stop();
  });

  it('returns a product by id', async () => {
    const productFixture = {
      id: 'prod-1',
      name: 'Wireless Keyboard',
      slug: 'wireless-keyboard',
      price: 79.99,
      currency: 'USD',
      stockCount: 5,
      categoryId: 'cat-1',
      tags: ['electronics'],
      status: 'PUBLISHED',
      description: null,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    const ctx = buildTestContext();
    (ctx.prisma.product.findUnique as jest.Mock).mockResolvedValue(productFixture);
    (ctx.loaders.category.load as jest.Mock).mockResolvedValue({
      id: 'cat-1',
      name: 'Electronics',
      slug: 'electronics',
    });

    const response = await server.executeOperation(
      {
        query: `
          query GetProduct($id: UUID!) {
            product(id: $id) {
              id
              name
              price
              inStock
              category { name }
            }
          }
        `,
        variables: { id: 'prod-1' },
      },
      { contextValue: ctx },
    );

    expect(response.body.kind).toBe('single');
    const data = (response.body as any).singleResult.data;

    expect(data.product.id).toBe('prod-1');
    expect(data.product.name).toBe('Wireless Keyboard');
    expect(data.product.inStock).toBe(true); // stockCount > 0
    expect(data.product.category.name).toBe('Electronics');
    expect(ctx.prisma.product.findUnique).toHaveBeenCalledWith({ where: { id: 'prod-1' } });
  });

  it('returns null for a non-existent product', async () => {
    const ctx = buildTestContext();
    (ctx.prisma.product.findUnique as jest.Mock).mockResolvedValue(null);

    const response = await server.executeOperation(
      { query: `query { product(id: "does-not-exist") { id name } }` },
      { contextValue: ctx },
    );

    const data = (response.body as any).singleResult.data;
    expect(data.product).toBeNull();
  });

  it('rejects createProduct without an editor role', async () => {
    const ctx = buildTestContext({
      user: { id: 'user-2', email: '[email protected]', role: 'VIEWER' },
    });

    const response = await server.executeOperation(
      {
        query: `
          mutation {
            createProduct(input: {
              name: "Test Product"
              slug: "test-product"
              price: 9.99
              currency: "USD"
              categoryId: "cat-1"
            }) {
              id
            }
          }
        `,
      },
      { contextValue: ctx },
    );

    const errors = (response.body as any).singleResult.errors;
    expect(errors).toBeDefined();
    expect(errors[0].extensions.code).toBe('FORBIDDEN');
  });
});

Integration Testing with Testcontainers

Unit tests with mocked Prisma are fast but do not catch query construction bugs. Integration tests run against a real PostgreSQL instance inside a Docker container.

Testing the Python Subgraph

Strawberry provides schema.execute for in-process testing without an HTTP server.

DataLoader Performance Verification

A key sanity check: verify that DataLoader actually batches queries. I add a query counter to the DataLoader batch function in test mode:

Query Complexity Limiting

Without limits, a client can craft deeply nested queries that multiply into thousands of database calls. The graphql-query-complexity library calculates a cost score before execution and rejects queries that exceed the limit.

Mark expensive fields with a higher complexity cost in the schema:

Register the plugin:

Persisted Queries

Persisted queries are a significant production optimisation — the client sends a hash instead of the full query document. This reduces payload size and enables server-side allowlisting (forbid ad-hoc queries in production).

Apollo Client supports automatic persisted queries (APQ) via the createPersistedQueryLink:

On the first request: the client sends the hash. The server has not seen it yet and returns a PersistedQueryNotFound. The client then sends the full query. On subsequent requests: only the hash is sent and the server uses the cached document.

Deployment

Health Checks

The TypeScript subgraph exposes Prisma connectivity as a health endpoint:

The Python service already has /health returning {"status": "ok"} from Part 6.

Environment Variables

Variable
Service
Purpose

DATABASE_URL

TypeScript + Python

PostgreSQL connection string

JWT_SECRET

TypeScript + Python

HMAC key for JWT verification

PORT

TypeScript

Listening port (default 4001)

NODE_ENV

TypeScript

production disables stack traces in errors

CI Supergraph Composition

Series Summary

Over these seven parts, a complete GraphQL stack has been built from the ground up:

Part
What Was Covered

1 — Introduction

GraphQL concepts, SDL, Apollo Server 4 setup, first query execution

2 — Schema Design

Interfaces, unions, enums, custom scalars, pagination patterns, schema evolution

3 — Building the API

Apollo Server + Prisma, DataLoader, Zod validation, subscriptions, cursor pagination

4 — Clients

TypeScript with code generation, Python gql client, subscriptions from Python

5 — Auth

JWT context, resolver guards, schema directives, Strawberry permissions

6 — Federation

Apollo Federation v2, TypeScript and Python subgraphs, Apollo Router gateway

7 — Production

Testing strategies, DataLoader verification, query complexity, persisted queries, deployment

The federated architecture scales well — new services can join the supergraph as independent subgraphs without touching existing services. Each team owns their part of the graph, and the router composes them transparently for clients.

Last updated