HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•175 Minutes

Apollo Server and Production Node.js Integration

Move from GraphQL fundamentals to a structured Apollo Server application, including HTTP integration, middleware, context, configuration, modular schemas, and production concerns.

Understanding Apollo Server

Framework:Apollo ServerRuntime:Node.js

Apollo Server is a GraphQL server implementation for JavaScript and TypeScript applications. It handles the GraphQL-specific parts of an API while allowing your application to control authentication, business logic, persistence, logging, and infrastructure.

A useful mental model is:

HTTP Request
    ↓
Node.js HTTP Application
    ↓
Apollo Server
    ↓
GraphQL Parse + Validate + Execute
    ↓
Resolvers
    ↓
Application Services
    ↓
Data Sources

Apollo Server does not replace Node.js or your application architecture. It provides the GraphQL execution layer.

A typical Apollo Server configuration contains:

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

The typeDefs describe the GraphQL schema and the resolvers provide runtime behavior.

In a small application, schema and resolver definitions may live in a few files. In a production application, they are usually divided into domain modules.

Apollo Server also provides integrations for request context, error formatting, plugins, observability, and other server concerns. The exact APIs can change between major versions, so production applications should follow the documentation for the installed version rather than copying old setup code blindly.

Apollo Server Architecture
Key Takeaways
  • Apollo Server provides the GraphQL server execution layer.
  • It does not replace your application's business or persistence layers.
  • Schema definitions and resolvers are the core Apollo Server inputs.
  • Always align setup code with the installed Apollo Server version.
Knowledge Check

What is Apollo Server primarily responsible for in a Node.js GraphQL application?

Connecting Schemas and Resolvers

Apollo Server connects a GraphQL schema with resolver implementations.

Consider this schema:

type Query {
  hello: String!
  user(id: ID!): User
}

type User {
  id: ID!
  name: String!
}

The resolver map can mirror the schema structure:

const resolvers = {
  Query: {
    hello: () => "Hello GraphQL",
    user: (_, { id }, context) => {
      return context.userService.findById(id);
    }
  }
};

The Query property corresponds to the root Query type. The hello and user properties correspond to fields on that type.

Nested types can have their own resolver maps:

const resolvers = {
  Query: {
    user: (_, { id }, context) => context.userService.findById(id)
  },
  User: {
    fullName: (user) => `${user.firstName} ${user.lastName}`
  }
};

The resolver map therefore acts as a runtime implementation of the schema.

A useful rule is to keep schema definitions descriptive and resolver implementations focused. Avoid putting database queries, authentication parsing, validation, transformation, and business workflows into a single resolver function.

Schema and Resolver Map
const typeDefs = `
  type User {
    id: ID!
    name: String!
  }

  type Query {
    user(id: ID!): User
  }
`;

const resolvers = {
  Query: {
    user: (_, { id }, context) => {
      return context.userService.findById(id);
    }
  }
};
💡 The Query.user resolver receives the requested ID and delegates data retrieval to an application service. The GraphQL layer therefore remains separate from the underlying persistence implementation.
Key Takeaways
  • Resolver maps mirror GraphQL schema types and fields.
  • Root operations are implemented under Query, Mutation, and Subscription.
  • Nested object types can have their own resolver maps.
  • Keep resolver functions focused on orchestration.
Knowledge Check

Where is the resolver for a field named `user` on the root `Query` type normally placed?

Integrating GraphQL with an HTTP Server

In production Node.js applications, GraphQL often needs to coexist with other HTTP routes and middleware.

For example, an application may expose:

GET  /health
POST /webhooks/payment
GET  /metrics
POST /graphql

GraphQL is therefore one part of the HTTP application rather than necessarily being the entire application.

The integration layer is responsible for concerns such as:

  • HTTP request handling
  • CORS
  • authentication middleware
  • request IDs
  • compression
  • body parsing where appropriate
  • rate limiting
  • health checks
  • metrics
  • GraphQL request handling

A typical architecture is:

Node HTTP Server
   ├── /health
   ├── /metrics
   ├── /webhooks
   └── /graphql
             ↓
        Apollo Server

This separation is useful because webhooks and health endpoints often have requirements that differ from GraphQL operations.

For example, a payment webhook may require verification of the provider's signature against the raw request body, while GraphQL requests usually use JSON parsing. Applying identical middleware blindly to every route can introduce subtle bugs.

GraphQL Inside a Node.js HTTP Application
Key Takeaways
  • GraphQL can coexist with ordinary HTTP endpoints.
  • The HTTP layer can handle cross-cutting infrastructure concerns.
  • Different endpoints may require different middleware behavior.
  • Webhook verification may require special raw-body handling.
Knowledge Check

Why might a production Node.js application expose both `/health` and `/graphql`?

Building a Clean GraphQL Context

The GraphQL context is an important dependency boundary between the request and resolver execution.

A mature context might contain:

{
  requestId,
  user,
  services,
  repositories,
  loaders,
  logger
}

For example:

const context = async ({ req }) => {
  const user = await authenticate(req);

  return {
    requestId: req.headers["x-request-id"] ?? crypto.randomUUID(),
    user,
    services,
    repositories,
    loaders: createLoaders(),
    logger: createRequestLogger()
  };
};

This creates a request-scoped environment for resolver execution.

A common mistake is creating expensive resources on every request unnecessarily. Database connection pools, for example, are generally application-level resources rather than something that should be opened and closed for every GraphQL operation.

On the other hand, DataLoader instances and request-specific authorization information are commonly scoped per request.

Think carefully about lifecycle:

Application lifetime
  → DB pool
  → configuration
  → reusable stateless services

Request lifetime
  → authenticated user
  → request ID
  → DataLoader cache
  → request logger

Correct lifecycle management is important for both performance and security.

Context Lifecycle
Request ID: Request-scoped
Identifies a single request across logs and downstream calls.
Authenticated user: Request-scoped
Represents the identity associated with the current request.
DataLoader: Usually request-scoped
Prevents cached values from accidentally crossing request or user boundaries.
Database pool: Application-scoped
A connection pool is generally reused rather than recreated per request.
Key Takeaways
  • Context should contain useful request-scoped dependencies.
  • Not every dependency belongs in request scope.
  • Database pools are generally application-scoped.
  • Request-specific DataLoader instances should normally not be globally shared.
Knowledge Check

Which dependency is generally appropriate to create per GraphQL request?

Apollo Server Plugins and Lifecycle Hooks

Production GraphQL systems often need observability and lifecycle behavior around requests.

Apollo Server provides a plugin architecture that can be used for concerns such as:

  • request timing
  • logging
  • tracing
  • metrics
  • usage reporting
  • startup and shutdown behavior

Conceptually, a plugin can observe a request lifecycle:

Request begins
    ↓
Operation parsed
    ↓
Validation
    ↓
Execution
    ↓
Response created
    ↓
Request completed

This is useful when you need consistent instrumentation without adding logging code to every resolver.

For example, a request logger can record:

requestId=abc123
operation=GetDashboard
duration=84ms
status=success

Resolver-level logs can then add details for individual operations.

Avoid logging entire GraphQL variables blindly. Variables can contain passwords, access tokens, personal data, or other sensitive values.

A good observability strategy records enough information to diagnose production behavior while deliberately excluding secrets and sensitive payloads.

Never log secrets by default

GraphQL variables can contain credentials, tokens, passwords, personal information, and payment-related data. Observability code should use explicit redaction rather than logging complete request payloads.

GraphQL Request Observability
Key Takeaways
  • Plugins can provide cross-cutting GraphQL lifecycle behavior.
  • Observability should be centralized where possible.
  • Operation names and request IDs are useful diagnostic metadata.
  • GraphQL variables should be treated as potentially sensitive.
Knowledge Check

Why should GraphQL observability systems avoid blindly logging all variables?