GraphQL
Apollo Server with Express
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:
- Create an Express + Apollo Server setup with CORS enabled
- Add a custom middleware that logs the request time for all requests
- Configure the context to extract a user from an Authorization header
- Add a REST endpoint at
/api/statusthat returns server health - Serve a static HTML file at the root path alongside your GraphQL endpoint