Introduction
In our previous lecture, we compared REST and GraphQL, exploring their fundamental differences, strengths, and use cases. Today, we'll take a deep dive into Apollo Server, one of the most popular and powerful tools for implementing GraphQL APIs in a Node.js environment.
Apollo Server provides a robust, production-ready framework that makes it easy to build a GraphQL API that works with any data source, schema, or client. By the end of this lecture, you'll understand how to set up Apollo Server, define a schema, implement resolvers, and configure various features to enhance your GraphQL API.
What is Apollo Server?
Apollo Server is an open-source, spec-compliant GraphQL server that's compatible with any GraphQL client, including Apollo Client. It's built on Node.js and provides:
- GraphQL Schema Implementation: Tools to define and serve your GraphQL schema
- Request Pipeline: Processing incoming GraphQL requests and returning responses
- Developer Experience: GraphQL Playground/Apollo Studio for API exploration and testing
- Production Features: Caching, monitoring, error handling, etc.
- Extensibility: Plugins system for customizing behavior
graph TD
A[Client Applications] -->|GraphQL Queries| B[Apollo Server]
B -->|Schema Definition| C[Type Definitions]
B -->|Data Fetching| D[Resolvers]
D -->|Data Access| E[Database]
D -->|Data Access| F[REST APIs]
D -->|Data Access| G[Microservices]
D -->|Data Access| H[Other Data Sources]
Apollo Server vs Other GraphQL Servers
While several GraphQL server implementations exist, Apollo Server is particularly popular because of its:
- Comprehensive Feature Set: Out-of-the-box support for most GraphQL use cases
- Active Development: Frequent updates and improvements
- Strong Community: Large community of users and contributors
- Enterprise Support: Commercial support available through Apollo Graph, Inc.
- Integration Ecosystem: Works seamlessly with other Apollo tools
Comparison of GraphQL Server Implementations
| Feature | Apollo Server | Express-GraphQL | graphql-yoga |
|---|---|---|---|
| Developer Experience | Excellent | Good | Excellent |
| Production Features | Comprehensive | Basic | Good |
| Customization | High | Medium | High |
| Framework Integration | Multiple (Express, Koa, etc.) | Express only | Multiple |
| Active Development | Very Active | Maintenance Mode | Active |
Setting Up Apollo Server
Installation
Let's start by setting up a new Apollo Server project:
Setting Up a New Project
# Create a new directory
mkdir apollo-server-demo
cd apollo-server-demo
# Initialize a new Node.js project
npm init -y
# Install dependencies
npm install apollo-server graphql
Basic Apollo Server Setup
Here's a minimal Apollo Server implementation:
server.js - Basic Setup
const { ApolloServer, gql } = require('apollo-server');
// Define schema using GraphQL Schema Definition Language (SDL)
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
year: Int
}
type Query {
books: [Book]
book(id: ID!): Book
}
`;
// Sample data
const books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 },
{ id: '3', title: '1984', author: 'George Orwell', year: 1949 }
];
// Define resolvers
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find(book => book.id === id)
}
};
// Create an Apollo Server instance
const server = new ApolloServer({ typeDefs, resolvers });
// Start the server
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Running the Server
To start the server, run:
node server.js
You should see a message that your server is running, typically at http://localhost:4000. Navigate to this URL in your browser to access the Apollo Sandbox, an in-browser IDE for exploring your GraphQL API.
Analogy: Restaurant Menu
Setting up Apollo Server is like creating a restaurant menu and kitchen:
- Type Definitions (typeDefs) are like the menu that lists all available dishes (types) and their ingredients (fields)
- Resolvers are like the kitchen staff who know how to prepare each dish when ordered
- Apollo Server is like the restaurant building itself that houses everything and serves customers
- GraphQL Playground/Apollo Sandbox is like the ordering system where customers can browse the menu and place orders
Just as a restaurant needs both a menu and chefs to function, a GraphQL API needs both type definitions and resolvers.
Defining Your Schema
The schema is the foundation of any GraphQL API. It defines the types, fields, and relationships that make up your API's capabilities. In Apollo Server, we define the schema using the GraphQL Schema Definition Language (SDL).
Basic Type Definitions
Let's expand our schema to include more types and relationships:
Expanded Schema
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: Author!
genre: String
year: Int
reviews: [Review]
}
type Author {
id: ID!
name: String!
bio: String
books: [Book!]!
}
type Review {
id: ID!
rating: Int!
text: String
user: String!
book: Book!
}
type Query {
books: [Book]
book(id: ID!): Book
authors: [Author]
author(id: ID!): Author
}
`;
Schema Types
GraphQL schema supports several types of definitions:
- Object Types: Define objects with fields (e.g.,
type Book) - Scalar Types: Primitive types like
String,Int,Boolean,ID - Enum Types: Define a set of allowed values
- Input Types: Special object types for arguments
- Interface and Union Types: Abstract types for polymorphism
Complex Schema Example
const typeDefs = gql`
# Enum type for book genres
enum Genre {
FICTION
NON_FICTION
SCIENCE_FICTION
FANTASY
MYSTERY
BIOGRAPHY
}
# Interface for items that can be reviewed
interface Reviewable {
id: ID!
reviews: [Review]
}
# Object types
type Book implements Reviewable {
id: ID!
title: String!
author: Author!
genre: Genre
year: Int
reviews: [Review]
}
type Author {
id: ID!
name: String!
bio: String
books: [Book!]!
}
type Review {
id: ID!
rating: Int!
text: String
user: String!
item: Reviewable!
}
# Input type for creating books
input BookInput {
title: String!
authorId: ID!
genre: Genre
year: Int
}
type Query {
books(genre: Genre): [Book]
book(id: ID!): Book
authors: [Author]
author(id: ID!): Author
}
type Mutation {
addBook(book: BookInput!): Book
addReview(itemId: ID!, rating: Int!, text: String, user: String!): Review
}
`;
Best Practices for Schema Design
- Use Descriptive Names: Make type and field names clear and self-documenting
- Be Consistent: Follow a naming convention (typically camelCase for fields)
- Use Non-Nullable Fields (!): When a field will always have a value
- Add Descriptions: Include comments to document your schema
- Design for Consumers: Focus on how the API will be used, not on your data structure
- Follow the Principle of Least Astonishment: Make your API behave as users would expect
Schema With Descriptions
const typeDefs = gql`
"""
A book in the library
"""
type Book {
"Unique identifier for the book"
id: ID!
"Title of the book"
title: String!
"Author who wrote the book"
author: Author!
"Genre of the book"
genre: String
"Year the book was published"
year: Int
}
`;
Implementing Resolvers
Resolvers are functions that fetch the data for each field in your schema. They determine how the fields in your schema are populated with actual data.
Resolver Structure
Resolvers match the structure of your schema types and fields:
Basic Resolver Structure
const resolvers = {
Query: {
books: (parent, args, context, info) => {
// Return array of books
},
book: (parent, args, context, info) => {
// Return a single book
}
},
Book: {
author: (parent, args, context, info) => {
// Return author for this book
}
}
};
Resolver Arguments
Each resolver receives four arguments:
- parent: The result of the parent resolver (also called root or obj)
- args: The arguments provided to the field
- context: A shared object available to all resolvers (often contains auth info, data sources)
- info: Contains information about the execution state of the query
Implementing Resolvers for Our Schema
Let's implement resolvers for our expanded schema:
Expanded Resolvers
// Sample data
const books = [
{ id: '1', title: 'The Great Gatsby', authorId: '1', genre: 'FICTION', year: 1925 },
{ id: '2', title: 'To Kill a Mockingbird', authorId: '2', genre: 'FICTION', year: 1960 },
{ id: '3', title: '1984', authorId: '3', genre: 'SCIENCE_FICTION', year: 1949 }
];
const authors = [
{ id: '1', name: 'F. Scott Fitzgerald', bio: 'American novelist...' },
{ id: '2', name: 'Harper Lee', bio: 'American novelist...' },
{ id: '3', name: 'George Orwell', bio: 'English novelist...' }
];
const reviews = [
{ id: '1', bookId: '1', rating: 5, text: 'A masterpiece!', user: 'reader1' },
{ id: '2', bookId: '1', rating: 4, text: 'Beautifully written.', user: 'reader2' },
{ id: '3', bookId: '2', rating: 5, text: 'A classic.', user: 'reader1' }
];
// Resolvers
const resolvers = {
Query: {
books: (_, { genre }) => {
if (genre) {
return books.filter(book => book.genre === genre);
}
return books;
},
book: (_, { id }) => books.find(book => book.id === id),
authors: () => authors,
author: (_, { id }) => authors.find(author => author.id === id)
},
Book: {
author: (parent) => authors.find(author => author.id === parent.authorId),
reviews: (parent) => reviews.filter(review => review.bookId === parent.id)
},
Author: {
books: (parent) => books.filter(book => book.authorId === parent.id)
},
Review: {
book: (parent) => books.find(book => book.id === parent.bookId)
},
Mutation: {
addBook: (_, { book }) => {
const newBook = {
id: String(books.length + 1),
title: book.title,
authorId: book.authorId,
genre: book.genre,
year: book.year
};
books.push(newBook);
return newBook;
},
addReview: (_, { itemId, rating, text, user }) => {
const newReview = {
id: String(reviews.length + 1),
bookId: itemId,
rating,
text,
user
};
reviews.push(newReview);
return newReview;
}
}
};
Resolver Best Practices
- Keep Resolvers Simple: Delegate complex logic to separate services/functions
- Use DataLoader for Batching: Prevent N+1 query problems
- Handle Errors Gracefully: Use try/catch and return meaningful errors
- Add Proper Documentation: Comment complex resolvers
- Leverage Context: Pass shared resources through context
Setting up Context
The context is a shared object that's available to all resolvers. It's typically used to pass request-specific data like authentication information, data sources, or utility functions.
Creating a Context Function
Basic Context Setup
const { ApolloServer } = require('apollo-server');
// Create data sources (these could be database connections, API clients, etc.)
const dataSources = {
books: {
getAll: () => books,
getById: (id) => books.find(book => book.id === id),
create: (book) => {
const newBook = { id: String(books.length + 1), ...book };
books.push(newBook);
return newBook;
}
},
authors: {
getAll: () => authors,
getById: (id) => authors.find(author => author.id === id)
}
};
// Context function
const context = ({ req }) => {
// Get the user token from the headers
const token = req.headers.authorization || '';
// Verify the token and get user info
// This is a simplified example - in a real app, you'd use JWT or similar
const user = token ? { id: '123', name: 'Test User', isAdmin: true } : null;
// Add the user and data sources to the context
return {
user,
dataSources
};
};
// Create the Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
context
});
Using Context in Resolvers
Resolvers with Context
const resolvers = {
Query: {
books: (_, args, { dataSources }) => {
return dataSources.books.getAll();
},
book: (_, { id }, { dataSources }) => {
return dataSources.books.getById(id);
}
},
Mutation: {
addBook: (_, { book }, { user, dataSources }) => {
// Check if user is authenticated
if (!user) {
throw new Error('You must be logged in to add a book');
}
return dataSources.books.create({
...book,
createdBy: user.id
});
}
}
};
Apollo Data Sources
Apollo provides a dedicated pattern for data sources that helps with caching, deduplication, and error handling:
Using Apollo Data Sources
const { ApolloServer } = require('apollo-server');
const { RESTDataSource } = require('apollo-datasource-rest');
class BooksAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/';
}
async getBooks() {
return this.get('books');
}
async getBook(id) {
return this.get(`books/${id}`);
}
async createBook(book) {
return this.post('books', book);
}
}
// Create the Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
booksAPI: new BooksAPI()
})
});
graph TD
A[Client Request] --> B[Apollo Server]
B --> C[Context Creation]
C --> D[Authentication]
C --> E[Data Sources Initialization]
C --> F[Request-specific Data]
D --> G[Resolver Execution]
E --> G
F --> G
G --> H[Response]
Implementing Mutations
Mutations are operations that modify data. In GraphQL, mutations are explicitly named to indicate their purpose.
Defining Mutations in the Schema
Mutation Schema
const typeDefs = gql`
type Book {
id: ID!
title: String!
authorId: ID!
genre: String
year: Int
}
input BookInput {
title: String!
authorId: ID!
genre: String
year: Int
}
type Mutation {
addBook(book: BookInput!): Book
updateBook(id: ID!, book: BookInput!): Book
deleteBook(id: ID!): Boolean
}
`;
Implementing Mutation Resolvers
Mutation Resolvers
const resolvers = {
// ... Query resolvers
Mutation: {
addBook: (_, { book }, { dataSources, user }) => {
// Check authentication
if (!user) throw new Error('You must be logged in');
return dataSources.books.create(book);
},
updateBook: async (_, { id, book }, { dataSources, user }) => {
// Check authentication
if (!user) throw new Error('You must be logged in');
// Check if book exists
const existingBook = await dataSources.books.getById(id);
if (!existingBook) throw new Error(`Book with ID ${id} not found`);
// Update book
return dataSources.books.update(id, book);
},
deleteBook: async (_, { id }, { dataSources, user }) => {
// Check authentication and permissions
if (!user) throw new Error('You must be logged in');
if (!user.isAdmin) throw new Error('Only admins can delete books');
// Check if book exists
const existingBook = await dataSources.books.getById(id);
if (!existingBook) throw new Error(`Book with ID ${id} not found`);
// Delete book
return dataSources.books.delete(id);
}
}
};
Mutation Best Practices
- Use Input Types: Group related input fields into input types
- Return Modified Objects: Return the object that was modified by the mutation
- Use Clear Naming: Name mutations with verb prefixes (add, create, update, delete)
- Validate Input: Perform thorough validation before processing
- Handle Errors: Return meaningful error messages
- Enforce Permissions: Check authentication and authorization
sequenceDiagram
participant Client
participant ApolloServer
participant InputValidation
participant Authentication
participant DataSource
participant Database
Client->>ApolloServer: mutation { addBook(book: {...}) }
ApolloServer->>InputValidation: Validate input
ApolloServer->>Authentication: Check permissions
ApolloServer->>DataSource: Process mutation
DataSource->>Database: Insert data
Database-->>DataSource: Return result
DataSource-->>ApolloServer: Return created object
ApolloServer-->>Client: Return response
Error Handling in Apollo Server
Proper error handling is crucial for a robust GraphQL API. Apollo Server provides several ways to handle and customize errors.
Basic Error Handling
Throwing Errors in Resolvers
const resolvers = {
Query: {
book: (_, { id }) => {
const book = books.find(book => book.id === id);
if (!book) {
throw new Error(`Book with ID ${id} not found`);
}
return book;
}
}
};
Apollo Error Codes
Apollo Server provides predefined error codes for common scenarios:
Using Apollo Error Codes
const { ApolloServer, gql, UserInputError, AuthenticationError, ForbiddenError } = require('apollo-server');
const resolvers = {
Mutation: {
addBook: (_, { book }, { user }) => {
// Validate input
if (!book.title) {
throw new UserInputError('Title is required', {
invalidArgs: ['title']
});
}
// Check authentication
if (!user) {
throw new AuthenticationError('You must be logged in');
}
// Check permissions
if (!user.canAddBooks) {
throw new ForbiddenError('You do not have permission to add books');
}
// Process mutation...
}
}
};
Custom Error Formatting
You can customize how errors are formatted in the response:
Custom Error Formatting
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (err) => {
// Don't expose internal server errors to clients
if (err.message.startsWith('Database error:')) {
return new Error('Internal server error');
}
// Add custom error data
if (err.extensions?.code === 'FORBIDDEN') {
err.extensions.customData = {
resourceType: err.path?.[0],
requiredPermission: 'ADMIN'
};
}
// Log error for server-side debugging
console.error('GraphQL Error:', err);
return err;
}
});
Extending Error Classes
You can create custom error classes for specific error scenarios:
Custom Error Classes
const { ApolloError } = require('apollo-server');
// Custom error class
class ResourceNotFoundError extends ApolloError {
constructor(resource, id) {
super(`${resource} with ID ${id} not found`, 'RESOURCE_NOT_FOUND', {
resource,
id
});
}
}
// In resolver
const resolvers = {
Query: {
book: (_, { id }) => {
const book = books.find(book => book.id === id);
if (!book) {
throw new ResourceNotFoundError('Book', id);
}
return book;
}
}
};
GraphQL Subscriptions
Subscriptions enable real-time functionality in GraphQL. They allow clients to receive updates when specific events occur on the server.
Setting Up Subscriptions
Basic Subscription Setup
const { ApolloServer, gql, PubSub } = require('apollo-server');
// Create a PubSub instance
const pubsub = new PubSub();
// Event channels
const BOOK_ADDED = 'BOOK_ADDED';
const BOOK_UPDATED = 'BOOK_UPDATED';
// Schema with subscriptions
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
}
type Subscription {
bookAdded: Book
bookUpdated: Book
}
type Query {
books: [Book]
}
type Mutation {
addBook(title: String!, author: String!): Book
updateBook(id: ID!, title: String, author: String): Book
}
`;
// Sample data
const books = [
{ id: '1', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald' }
];
// Resolvers
const resolvers = {
Query: {
books: () => books
},
Mutation: {
addBook: (_, { title, author }) => {
const newBook = { id: String(books.length + 1), title, author };
books.push(newBook);
// Publish event to subscribers
pubsub.publish(BOOK_ADDED, { bookAdded: newBook });
return newBook;
},
updateBook: (_, { id, title, author }) => {
const bookIndex = books.findIndex(book => book.id === id);
if (bookIndex === -1) {
throw new Error(`Book with ID ${id} not found`);
}
const updatedBook = {
...books[bookIndex],
title: title || books[bookIndex].title,
author: author || books[bookIndex].author
};
books[bookIndex] = updatedBook;
// Publish event to subscribers
pubsub.publish(BOOK_UPDATED, { bookUpdated: updatedBook });
return updatedBook;
}
},
Subscription: {
bookAdded: {
subscribe: () => pubsub.asyncIterator([BOOK_ADDED])
},
bookUpdated: {
subscribe: () => pubsub.asyncIterator([BOOK_UPDATED])
}
}
};
// Create and start the server
const server = new ApolloServer({
typeDefs,
resolvers,
subscriptions: {
path: '/subscriptions'
}
});
server.listen().then(({ url, subscriptionsUrl }) => {
console.log(`🚀 Server ready at ${url}`);
console.log(`🔔 Subscriptions ready at ${subscriptionsUrl}`);
});
Subscription with Filtering
You can filter subscription events based on criteria:
Filtered Subscriptions
// Schema with filtered subscription
const typeDefs = gql`
type Subscription {
bookAdded: Book
bookUpdated: Book
bookByAuthorUpdated(author: String!): Book
}
`;
// Resolver with filtering
const resolvers = {
Subscription: {
// ...other subscription resolvers
bookByAuthorUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator([BOOK_UPDATED]),
(payload, variables) => {
// Only send event if the book's author matches the subscription variable
return payload.bookUpdated.author === variables.author;
}
)
}
}
};
sequenceDiagram
participant Client1 as Client 1
participant Client2 as Client 2
participant ApolloServer
participant PubSub
Client1->>ApolloServer: subscription { bookAdded }
Client2->>ApolloServer: subscription { bookByAuthorUpdated(author: "Orwell") }
ApolloServer->>PubSub: asyncIterator(BOOK_ADDED)
ApolloServer->>PubSub: asyncIterator(BOOK_UPDATED)
Note over Client1, PubSub: WebSocket connections established
ApolloServer->>PubSub: publish(BOOK_ADDED, { newBook })
PubSub->>Client1: { bookAdded: newBook }
ApolloServer->>PubSub: publish(BOOK_UPDATED, { updatedBook })
Note over PubSub, Client2: Filter checks author === "Orwell"
PubSub->>Client2: { bookByAuthorUpdated: updatedBook } (if author matches)
Using Redis for PubSub
For production environments, it's recommended to use a dedicated PubSub system like Redis:
Redis PubSub
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
// Create Redis clients
const options = {
host: 'localhost',
port: 6379
};
const pubsub = new RedisPubSub({
publisher: new Redis(options),
subscriber: new Redis(options)
});
// Use pubsub as before
Advanced Apollo Server Features
Apollo Server Plugins
Plugins allow you to customize Apollo Server's behavior at different stages of the request lifecycle:
Custom Logger Plugin
const loggerPlugin = {
// Fires when a GraphQL request is received
requestDidStart(requestContext) {
console.log('Request started:', requestContext.request.operationName);
const startTime = Date.now();
return {
// Fires when parsing was successful
didResolveOperation(context) {
console.log('Operation resolved:', context.operationName);
},
// Fires right before responses are sent
willSendResponse(context) {
const duration = Date.now() - startTime;
console.log(`Request for ${context.operationName} completed in ${duration}ms`);
},
// Fires when an error occurs
didEncounterErrors(context) {
console.error('Errors occurred:', context.errors);
}
};
}
};
// Add the plugin to Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [loggerPlugin]
});
Response Caching
Apollo Server includes built-in support for response caching:
Response Caching
const { ApolloServer } = require('apollo-server');
const responseCachePlugin = require('apollo-server-plugin-response-cache');
// Add cache hints to the schema
const typeDefs = gql`
type Book @cacheControl(maxAge: 3600) {
id: ID!
title: String!
author: String!
# This field changes frequently, so cache for a shorter time
rating: Float @cacheControl(maxAge: 60)
}
type Query {
books: [Book] @cacheControl(maxAge: 300)
book(id: ID!): Book
}
`;
// Create the server with caching
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [responseCachePlugin()],
cacheControl: {
defaultMaxAge: 30, // Default cache time in seconds
calculateHttpHeaders: true // Add Cache-Control headers to responses
}
});
Apollo Federation
Apollo Federation enables you to split your GraphQL schema across multiple services:
Federation Example
// In your books service
const { ApolloServer } = require('apollo-server');
const { buildFederatedSchema } = require('@apollo/federation');
const typeDefs = gql`
type Book @key(fields: "id") {
id: ID!
title: String!
authorId: ID!
}
extend type Query {
books: [Book]
book(id: ID!): Book
}
`;
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find(book => book.id === id)
},
Book: {
__resolveReference(book) {
return books.find(b => b.id === book.id);
}
}
};
// Create a federated schema
const server = new ApolloServer({
schema: buildFederatedSchema([{ typeDefs, resolvers }])
});
// In your authors service
const typeDefs = gql`
type Author @key(fields: "id") {
id: ID!
name: String!
bio: String
}
extend type Book @key(fields: "id") {
id: ID! @external
authorId: ID! @external
author: Author
}
extend type Query {
authors: [Author]
author(id: ID!): Author
}
`;
const resolvers = {
Query: {
authors: () => authors,
author: (_, { id }) => authors.find(a => a.id === id)
},
Author: {
__resolveReference(author) {
return authors.find(a => a.id === author.id);
}
},
Book: {
author(book) {
return authors.find(a => a.id === book.authorId);
}
}
};
// Create gateway that combines the services
const { ApolloGateway } = require('@apollo/gateway');
const gateway = new ApolloGateway({
serviceList: [
{ name: 'books', url: 'http://localhost:4001' },
{ name: 'authors', url: 'http://localhost:4002' }
]
});
const server = new ApolloServer({
gateway,
subscriptions: false // Federation doesn't yet support subscriptions
});
graph TD
A[Client] -->|GraphQL Query| B[Apollo Gateway]
B -->|Federation| C[Books Service]
B -->|Federation| D[Authors Service]
B -->|Federation| E[Reviews Service]
Production Deployment Considerations
Environment Configuration
Always use environment variables for sensitive information:
Using Environment Variables
require('dotenv').config();
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
// Use environment variables for sensitive information
const jwtSecret = process.env.JWT_SECRET;
// ...
}
});
Security Checklist
- Disable Introspection in Production: Prevents exposing schema details
- Set Maximum Query Depth: Prevents malicious deep queries
- Implement Rate Limiting: Protects against DoS attacks
- Use HTTPS: Encrypts traffic between clients and server
- Validate Input: Check all user input thoroughly
- Monitor and Log: Track usage and errors
Production Security Settings
const { ApolloServer } = require('apollo-server');
const { depthLimit } = require('graphql-depth-limit');
const { createRateLimitRule } = require('graphql-rate-limit');
// Create rate limit rule
const rateLimitRule = createRateLimitRule({
identifyContext: (context) => context.user?.id || context.req.ip,
formatError: () => 'Too many requests. Please try again later.',
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP/user to 100 requests per windowMs
});
// Create Apollo Server with production settings
const server = new ApolloServer({
typeDefs,
resolvers,
context,
validationRules: [
depthLimit(10), // Limit query depth
rateLimitRule // Apply rate limiting
],
introspection: process.env.NODE_ENV !== 'production', // Disable in production
playground: process.env.NODE_ENV !== 'production', // Disable in production
debug: process.env.NODE_ENV !== 'production', // Disable in production
formatError: (err) => {
// Log errors for server monitoring
console.error('GraphQL Error:', err);
// Don't expose internal server errors in production
if (process.env.NODE_ENV === 'production') {
if (err.message.startsWith('Database error:')) {
return new Error('Internal server error');
}
}
return err;
}
});
Monitoring and Observability
For production deployments, implement proper monitoring:
- Metrics: Track request counts, latency, error rates
- Logging: Log errors, slow queries, authentication attempts
- Tracing: Use Apollo Tracing or other APM tools
- Health Checks: Implement server health endpoints
Health Check Endpoint
const express = require('express');
const { ApolloServer } = require('apollo-server-express');
async function startApolloServer() {
const app = express();
// Health check endpoint
app.get('/health', (req, res) => {
res.status(200).send('OK');
});
// Readiness check endpoint
app.get('/ready', async (req, res) => {
try {
// Check database connection
await checkDatabaseConnection();
// Check other dependencies
await checkRedisConnection();
res.status(200).send('Ready');
} catch (error) {
console.error('Readiness check failed:', error);
res.status(503).send('Not Ready');
}
});
const server = new ApolloServer({
typeDefs,
resolvers
// ... other options
});
await server.start();
server.applyMiddleware({ app });
return { server, app };
}
startApolloServer().then(({ app }) => {
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
Practice Activities
Activity 1: Basic Apollo Server
Create a basic Apollo Server with the following features:
- Schema for a blog with posts, authors, and comments
- Resolvers for all query fields
- Mock data for testing
- Test queries using Apollo Sandbox
Activity 2: Implement Mutations
Extend your Apollo Server to include mutations:
- Add mutations for creating, updating, and deleting posts
- Add authentication using JWT
- Implement authorization checks in resolvers
- Test mutations using Apollo Sandbox
Activity 3: Add Subscriptions
Add real-time features to your Apollo Server:
- Implement subscriptions for new posts and comments
- Create filtered subscriptions based on author or category
- Test subscriptions using GraphQL Playground or Apollo Studio
Activity 4: Production-Ready Server
Optimize your Apollo Server for production:
- Add proper error handling and formatting
- Implement response caching
- Add security features (depth limiting, query complexity analysis)
- Create a health check endpoint
- Use environment variables for configuration
Summary
In this lecture, we've explored Apollo Server, a powerful framework for building GraphQL APIs:
- Setting up Apollo Server with type definitions and resolvers
- Designing a GraphQL schema with various types and relationships
- Implementing resolvers to fetch and manipulate data
- Using context for authentication, authorization, and shared resources
- Adding mutations for data modifications
- Handling errors gracefully
- Implementing subscriptions for real-time updates
- Exploring advanced features like plugins, caching, and federation
- Preparing for production deployment with security and monitoring considerations
Apollo Server provides a robust foundation for building GraphQL APIs that are flexible, efficient, and maintainable. By leveraging its features and following best practices, you can create APIs that are a joy for clients to consume and for developers to work with.