Apollo Server
Apollo Server is the most widely-used way to run a GraphQL API in Node. You give it a schema (type definitions) and resolvers, and it handles parsing, validating, and executing queries, plus production concerns like error formatting, plugins, and a built-in query explorer. It runs standalone or integrates with Express/Fastify. This page covers a minimal server, the context function (where auth and per-request data live), wiring it into Express, error handling, and the production settings — including the one you must turn off.
A minimal Apollo Server
Bash
npm install @apollo/server graphql
JS
import { ApolloServer } from '@apollo/server'
import { startStandaloneServer } from '@apollo/server/standalone'
// 1. Schema — the typed contract (SDL):
const typeDefs = `#graphql
type Book { title: String!, author: String! }
type Query { books: [Book!]! }
`
// 2. Resolvers — how each field gets its data:
const resolvers = {
Query: {
books: () => [{ title: 'Node Patterns', author: 'Ada' }],
},
}
// 3. Create and start the server:
const server = new ApolloServer({ typeDefs, resolvers })
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } })
console.log(`🚀 ready at ${url}`)🚀 ready at http://localhost:4000/ # Open it in a browser → Apollo Sandbox: an interactive query explorer with # schema docs and autocompletion.
Apollo Server takes `typeDefs` + `resolvers` and gives you a running, introspectable GraphQL endpoint
The two inputs are always the same: `typeDefs` (your [schema](/nodejs/graphql-schema) in SDL, the `#graphql` comment enables editor highlighting) and `resolvers` (the [functions](/nodejs/graphql-resolvers) that fetch each field). `startStandaloneServer` spins up an HTTP server with sensible defaults — ideal for getting started or for a service that's *only* GraphQL. Visiting the URL in a browser opens **Apollo Sandbox**, an interactive explorer that reads your schema via introspection and gives you autocompletion and inline docs to build and run queries. For real apps you usually mount Apollo onto an existing Express app instead, so GraphQL coexists with REST routes, health checks, and middleware.
The context function — per-request data and auth
JS
const server = new ApolloServer({ typeDefs, resolvers })
await startStandaloneServer(server, {
// 'context' runs ONCE PER REQUEST; its return value is passed to every resolver:
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '')
const user = token ? await verifyJwt(token) : null // may be null (anonymous)
return {
user, // resolvers read context.user to authorize
db, // share the db handle / DataLoaders here
dataloaders: createLoaders(db),
}
},
})`context` runs per request and is handed to every resolver — it's where auth, the DB, and DataLoaders belong
The **context** function executes once for each incoming request, and whatever it returns is passed as the third argument to *every* resolver in that request. This makes it the correct home for: the **authenticated user** (verify the [JWT](/nodejs/jwt)/session here, once, and let resolvers read `context.user` to authorize), shared handles like the **database** connection, and per-request **[DataLoaders](/nodejs/graphql-resolvers)** for batching. Doing auth in context (or in resolvers reading context) — rather than trying to bolt on per-route middleware — is the GraphQL way, since there's only one route. Keep context creation cheap; it runs on every request.
Integrating with Express
JS
import express from 'express'
import { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
const app = express()
const server = new ApolloServer({ typeDefs, resolvers })
await server.start() // must start before mounting
app.use(
'/graphql',
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({ user: await getUser(req) }),
}),
)
// GraphQL now lives alongside your REST routes, health checks, etc.:
app.get('/health', (req, res) => res.json({ status: 'ok' }))
app.listen(4000)Mount Apollo as Express middleware so GraphQL coexists with REST routes, health checks, and middleware
In most real applications you mount Apollo onto an [Express](/nodejs/express-intro) app via `expressMiddleware` rather than running it standalone. This lets your GraphQL endpoint live next to REST routes, [health checks](/nodejs/health-checks), static files, and shared middleware (logging, [Helmet](/nodejs/helmet), rate limiting). Note the order: call `await server.start()` before mounting the middleware, and put `express.json()` ahead of it so the request body is parsed. The `context` function works the same way, receiving the Express `req`. This integration is the standard production setup.
Error handling
JS
import { GraphQLError } from 'graphql'
const resolvers = {
Query: {
user: async (_, { id }, { user }) => {
if (!user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED', http: { status: 401 } },
})
}
const found = await db.users.findById(id)
if (!found) {
throw new GraphQLError('User not found', { extensions: { code: 'NOT_FOUND' } })
}
return found
},
},
}Throw `GraphQLError` with an `extensions.code` — errors go in the response's `errors` array, not the HTTP status
GraphQL responses can be **partial**: a query touching ten fields might succeed on nine and fail on one, so errors are returned in a top-level `errors` array *alongside* whatever `data` did resolve — rather than via the HTTP status code (a GraphQL response is usually `200` even with errors). Throw a `GraphQLError` with a machine-readable `extensions.code` (`UNAUTHENTICATED`, `FORBIDDEN`, `NOT_FOUND`, `BAD_USER_INPUT`) so clients can branch on the error type. Apollo catches thrown errors and formats them into that array, attaching which field and path failed.
Production settings
Disable introspection and the explorer in production — they expose your entire schema to attackers
By default Apollo enables **introspection** (the API can be queried for its full schema) and the Sandbox explorer — wonderful in development, a liability in production, where they hand an attacker a complete map of your API's types, fields, and operations to probe. Disable them in production (`introspection: process.env.NODE_ENV !== 'production'`). Layer on the other production protections: **query depth and complexity limits** (so a maliciously deep/expensive query can't DoS your server), **disable stack traces** in error responses (don't leak internals — Apollo masks them outside development by default), persisted/allow-listed queries for public APIs, and the usual [rate limiting](/nodejs/security-best-practices) and timeouts. GraphQL's flexibility is also an attack surface; lock it down before shipping.
Next
Design the contract that drives it all: [Schemas & Type Definitions](/nodejs/graphql-schema).