Lesson 3
Implementing Rate Limiting in GraphQL with Apollo Server 4
Deprecation Notice

This course path teaches Apollo Server 3, which has been deprecated. To access the new course paths covering Apollo Server 4, use this course path.

Introduction

In this lesson, we'll focus on enhancing the security of your GraphQL API by implementing Rate Limiting.

Rate limiting is a mechanism to control the number of requests a client can make to your API within a specific time frame. It is essential for:

  • Preventing Abuse: Protects your server from malicious users sending excessive requests, which could overload the system.
  • Ensuring Fair Usage: Limits access to resources, ensuring equitable distribution among users.
  • Enhancing Security: Acts as a defense mechanism against denial-of-service (DoS) attacks.
  • Improving Performance: Maintains consistent performance under high traffic by throttling excessive requests.

We'll use the graphql-rate-limit package along with graphql-shield for this task, integrating with Apollo Server 4. By the end of this lesson, you'll know how to apply rate limiting to your GraphQL API, improving its security, performance, and reliability.

Defining the GraphQL Schema

The schema defines the data structure and allowable queries. Here's a simple schema for a books example without direct rate limiting directives:

TypeScript
1const typeDefs = `#graphql 2 type Book { 3 id: ID! 4 title: String! 5 author: String! 6 } 7 8 type Query { 9 books: [Book] 10 } 11`; 12 13const resolvers = { 14 Query: { 15 books: () => books, 16 }, 17};
  • This schema includes a Book type and a books query to fetch book data.
Creating the GraphQL Server and Applying Rate Limiting

We'll use graphql-shield to apply rate limiting middleware to our schema, enabling fine-grained control over each query:

TypeScript
1import { ApolloServer } from '@apollo/server'; 2import { startStandaloneServer } from '@apollo/server/standalone'; 3import { makeExecutableSchema } from '@graphql-tools/schema'; 4import { applyMiddleware } from 'graphql-middleware'; 5import { shield } from 'graphql-shield'; 6import { createRateLimitRule } from 'graphql-rate-limit'; 7 8// Create schema 9const schema = makeExecutableSchema({ 10 typeDefs, 11 resolvers, 12}); 13 14// Define context interface 15interface MyContext { 16 ip: string; 17} 18 19// Create rate limit rule 20const rateLimitRule = createRateLimitRule({ 21 identifyContext: (ctx: MyContext) => ctx.ip, 22}); 23 24// Apply rate limit middleware 25const permissions = shield({ 26 Query: { 27 books: rateLimitRule({ 28 max: 3, 29 window: '15s', 30 }), 31 }, 32}); 33 34const schemaWithMiddleware = applyMiddleware(schema, permissions); 35 36const server = new ApolloServer({ 37 schema: schemaWithMiddleware, 38}); 39 40startStandaloneServer(server, { 41 listen: { port: 4000 }, 42 context: async ({ req }) => { 43 const ip = req.socket.remoteAddress || 'unknown'; 44 return { ip }; 45 }, 46}).then(({ url }) => { 47 console.log(`🚀 Server ready at ${url}`); 48});

The code starts by creating an executable schema using makeExecutableSchema, combining the defined type definitions and resolvers. A context interface, MyContext, is defined to capture the client's IP address, which is vital for identifying request sources. We use identifyContext with the client's IP address to differentiate users here, as there are no user's session or user's id in this particular demo example.

Rate limiting is enforced using createRateLimitRule, which tracks requests based on the client's IP. This rule is integrated into a permission layer via shield, applying a constraint on the books query to allow a maximum of 3 requests every 15 seconds per IP address. This limits how frequently a client can access the books data.

These constraints are applied to the schema through applyMiddleware, generating a schema with built-in rate limiting. An instance of ApolloServer is initialized with this schema, incorporating the defined rate limiting. The server is started using startStandaloneServer, listening on port 4000, and includes context configuration to correctly identify client IPs for rate limiting. A console message confirms server readiness.

Testing the Implementation

You can verify the rate limiting setup by running queries against the GraphQL API and ensuring limits are enforced:

TypeScript
1import fetch from 'node-fetch'; 2 3const query = ` 4 query { 5 books { 6 id 7 title 8 author 9 } 10 } 11`; 12 13const url = 'http://localhost:4000/'; 14 15(async () => { 16 for (let i = 0; i < 5; i++) { 17 try { 18 const response = await fetch(url, { 19 method: 'POST', 20 headers: { 21 'Content-Type': 'application/json', 22 }, 23 body: JSON.stringify({ query }), 24 }); 25 26 if (response.ok) { 27 const data = await response.json(); 28 console.log(JSON.stringify(data, null, 2)); 29 } else if (response.status === 429) { 30 const text = await response.text(); 31 console.error(`Error: ${text}`); 32 } 33 34 } catch (error) { 35 console.error('Error:', error); 36 } 37 } 38})();

Running this script demonstrates rate limiting in action, enforcing a maximum of three requests every 15 seconds with excess requests returning a 429 Too Many Requests response.

Lesson Summary

In this lesson, we demonstrated:

  • Creating a GraphQL schema and server with Apollo Server 4.
  • Integrating rate limiting using graphql-shield and the graphql-rate-limit rule.
  • Testing to confirm rate limits are effectively applied to queries by IP address.

By following these steps, you can better secure your GraphQL API against abuse and manage load efficiently.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.