GraphQL

Apollo Server with Express

16 min Lesson 7 of 35

Integrating Apollo Server with Express

While Apollo Server can run standalone, integrating it with Express.js gives you full control over your HTTP server, middleware, and routing. This is the most common production setup.

Basic Integration

Install the required packages:

npm install apollo-server-express express graphql npm install --save-dev @types/express

Create a basic Express + Apollo Server setup:

const express = require('express'); const { ApolloServer } = require('apollo-server-express'); const typeDefs = ` type Query { hello: String users: [User] } type User { id: ID! name: String! email: String! } `; const resolvers = { Query: { hello: () => 'Hello from Apollo Server with Express!', users: () => [ { id: '1', name: 'Alice', email: 'alice@example.com' }, { id: '2', name: 'Bob', email: 'bob@example.com' } ] } }; async function startServer() { const app = express(); const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); server.applyMiddleware({ app }); const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}${server.graphqlPath}`); }); } startServer();
Important: With Apollo Server 3+, you must call await server.start() before applying middleware with applyMiddleware().

Express Middleware

One major benefit of using Express is access to its rich middleware ecosystem:

const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const morgan = require('morgan'); const { ApolloServer } = require('apollo-server-express'); async function startServer() { const app = express(); // Security middleware app.use(helmet({ contentSecurityPolicy: process.env.NODE_ENV === 'production' ? undefined : false })); // CORS configuration app.use(cors({ origin: ['http://localhost:3000', 'https://myapp.com'], credentials: true })); // Logging middleware app.use(morgan('combined')); // Parse JSON bodies app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Custom middleware app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); next(); }); const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); server.applyMiddleware({ app, path: '/graphql' }); app.listen(4000); } startServer();
Using helmet() adds security headers to protect against common vulnerabilities. In development, you may need to disable CSP for GraphQL Playground to work.

Context Setup

The context is where you add shared data available to all resolvers. With Express, you can access the request object:

const server = new ApolloServer({ typeDefs, resolvers, context: ({ req, res }) => { // Get auth token from headers const token = req.headers.authorization || ''; // Verify token and get user const user = getUserFromToken(token); // Return context object return { user, req, res, db: database, // Database connection loaders: createLoaders() // DataLoaders }; } });
The context function runs once per request, before any resolvers execute. This is the perfect place to handle authentication and set up request-scoped resources.

CORS Configuration

When your frontend and backend are on different domains, you need CORS:

const cors = require('cors'); // Simple CORS - allow all origins (development only) app.use(cors()); // Production CORS - specific origins app.use(cors({ origin: function(origin, callback) { const allowedOrigins = [ 'http://localhost:3000', 'https://myapp.com', 'https://www.myapp.com' ]; if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, // Allow cookies methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // Apply Apollo Server middleware with CORS server.applyMiddleware({ app, cors: false, // Disable Apollo's CORS (use Express CORS instead) path: '/graphql' });
Security Warning: Never use cors() with no options in production. Always specify allowed origins to prevent unauthorized access.

Serving Static Files Alongside GraphQL

You can serve a frontend application and GraphQL API from the same Express server:

const express = require('express'); const path = require('path'); const { ApolloServer } = require('apollo-server-express'); async function startServer() { const app = express(); // Serve static files from React build app.use(express.static(path.join(__dirname, 'client/build'))); // API routes app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); // GraphQL endpoint const server = new ApolloServer({ typeDefs, resolvers }); await server.start(); server.applyMiddleware({ app, path: '/graphql' }); // Catch-all route - serve React app app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'client/build/index.html')); }); const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`Server: http://localhost:${PORT}`); console.log(`GraphQL: http://localhost:${PORT}/graphql`); }); } startServer();

Custom Route Handlers

You can mix REST endpoints with GraphQL on the same server:

// REST endpoints app.get('/api/users', async (req, res) => { const users = await db.user.findMany(); res.json(users); }); app.post('/api/upload', upload.single('file'), async (req, res) => { // Handle file upload res.json({ url: req.file.path }); }); // Webhooks app.post('/webhook/stripe', async (req, res) => { // Handle Stripe webhook res.json({ received: true }); }); // GraphQL endpoint server.applyMiddleware({ app, path: '/graphql' });

Environment-Specific Configuration

const isDevelopment = process.env.NODE_ENV === 'development'; const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => ({ req, db }), // Development features playground: isDevelopment, introspection: isDevelopment, debug: isDevelopment, // Production features formatError: (error) => { if (isDevelopment) { return error; } // Hide internal error messages in production return new Error('Internal server error'); } });
Disable introspection and playground in production for security. Use environment variables to toggle features between development and production.

Complete Production Setup

require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const { ApolloServer } = require('apollo-server-express'); const { typeDefs, resolvers } = require('./schema'); const { createContext } = require('./context'); async function startServer() { const app = express(); app.use(helmet()); app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true })); app.use(express.json({ limit: '10mb' })); const server = new ApolloServer({ typeDefs, resolvers, context: createContext, introspection: process.env.NODE_ENV !== 'production', playground: process.env.NODE_ENV !== 'production' }); await server.start(); server.applyMiddleware({ app, path: '/graphql', cors: false }); app.get('/health', (req, res) => res.json({ status: 'ok' })); const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`); }); } startServer().catch(console.error);
Practice Exercise:
  1. Create an Express + Apollo Server setup with CORS enabled
  2. Add a custom middleware that logs the request time for all requests
  3. Configure the context to extract a user from an Authorization header
  4. Add a REST endpoint at /api/status that returns server health
  5. Serve a static HTML file at the root path alongside your GraphQL endpoint