Apollo Server is the most widely deployed GraphQL server in the Node ecosystem. Apollo Server 4 (the current major line) dropped the heavy monolithic framework couplings in favor of a small core plus optional integrations. This guide gets a v4 server into production cleanly.
Standalone vs framework integration
Apollo Server 4 ships @apollo/server/standalone for quick starts and an Express integration for when you need middleware, custom routes, or a /healthz endpoint. For production I recommend the Express path — you will eventually want a non-GraphQL health check, and the standalone server makes that awkward.
npm install @apollo/server graphql express corsimport { ApolloServer } from '@apollo/server'
import { expressMiddleware } from '@apollo/server/express4'
import express from 'express'
import cors from 'cors'
const typeDefs = `#graphql
type Query { health: String! }
`
const resolvers = { Query: { health: () => 'ok' } }
const server = new ApolloServer({ typeDefs, resolvers })
await server.start()
const app = express()
app.get('/healthz', (_, res) => res.send('ok'))
app.use('/graphql', cors(), express.json(), expressMiddleware(server))
app.listen(process.env.PORT || 4000)The dedicated /healthz route returns instantly without touching the GraphQL execution path, which is exactly what an orchestrator's liveness probe wants.
Connecting a database
Resolvers need data. Inject your data sources through Apollo's context function so each request gets a clean handle:
const server = new ApolloServer<MyContext>({ typeDefs, resolvers })
// ...
expressMiddleware(server, {
context: async () => ({ db: pool }),
})Use a real connection pool (pg.Pool for Postgres) created once at module scope, never per-request. In production, the database URL comes from the environment. On PandaStack, attaching a managed PostgreSQL or MySQL database injects DATABASE_URL automatically, so your pg.Pool({ connectionString: process.env.DATABASE_URL }) just works without you copying credentials around.
The N+1 problem
The defining production issue with GraphQL is the N+1 query: a list field whose per-item resolver fires one query each. The fix is [DataLoader](https://github.com/graphql/dataloader), which batches and caches within a request:
import DataLoader from 'dataloader'
const userLoader = new DataLoader(async (ids) => {
const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids])
return ids.map((id) => rows.find((r) => r.id === id))
})Create loaders per request inside the context function, otherwise you leak cached data across users.
Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
EXPOSE 4000
CMD ["node", "dist/index.js"]Production checklist
| Setting | Recommendation |
|---|---|
| Introspection | Disable in prod (introspection: false) |
| Landing page | Use ApolloServerPluginLandingPageDisabled |
| Error masking | On by default in v4 — keep it |
| Query cost limits | Add graphql-query-complexity |
| Caching | @apollo/server-plugin-response-cache |
| Health check | Separate /healthz route |
Caching and performance
Apollo supports a full response cache and per-field cache hints via the @cacheControl directive. For read-heavy public APIs, response caching backed by Redis cuts database load dramatically. PandaStack offers managed Redis (via KubeBlocks) you can attach alongside your app for exactly this.
Also turn on persisted queries if you control the client — they shrink request payloads and let you allowlist known operations, blocking arbitrary queries at the edge.
Deploying
- 1Provision a managed database; attach it to the service.
- 2Connect your Git repo — the Dockerfile is detected and built.
- 3Confirm
/healthzresponds, then route traffic. - 4Add a custom domain; SSL is issued automatically.
- 5Watch live logs during the first rollout.
Rollbacks matter for a GraphQL API because a bad schema change can break every client at once. Keep deploy history enabled so you can revert to the previous image in one click while you fix forward.
References
- [Apollo Server 4 docs](https://www.apollographql.com/docs/apollo-server/)
- [DataLoader](https://github.com/graphql/dataloader)
- [graphql-query-complexity](https://github.com/slicknode/graphql-query-complexity)
- [Apollo response cache plugin](https://www.apollographql.com/docs/apollo-server/performance/caching/)
- [node-postgres pooling](https://node-postgres.com/features/pooling)
PandaStack's free tier gives you a container service, a managed database with auto-wired DATABASE_URL, and live logs to debug your first rollout. Start at [dashboard.pandastack.io](https://dashboard.pandastack.io).