Next.js
Updated
7 min read

GraphQL rate limits and bot detection with Yoga, Next.js & Arcjet

How to protect GraphQL backends using Arcjet. Implementing rate limiting and bot protection for Yoga + Next.js.

GraphQL rate limits and bot detection with Yoga, Next.js & Arcjet

Yoga is a GraphQL server that focuses on performance and developer experience and can be nicely integrated into Next.js. In this tutorial we will demonstrate how to integrate the Arcjet rate limiting and bot detection security functionality to protect your GraphQL backend.

Let's start with installing the key packages.

Installing Next.js

Next.js is a web application development framework. To install Next.js:

  1. Create a directory in which you would like to store the application: mkdir arcjet
  2. Enter the newly created directory: cd arcjet
  3. Download the latest version of Next.js and its dependencies into the directory: npx create-next-app@latest. You may receive a prompt stating: Need to install the following packages: create-next-app@x.x.x Ok to proceed? (y) - enter y to proceed.
  4. Select your desired options when prompted. To get started quickly, select the following options:

    What is your project named? yoga
    Would you like to use TypeScript? No / Yes
    Would you like to use ESLint? No / Yes
    Would you like to use Tailwind CSS? No / Yes
    Would you like your code inside a `src/` directory? No / Yes
    Would you like to use App Router? (recommended) No / Yes
    Would you like to customize the import alias (`@/*` by default)? No / Yes
  5. Navigate into the newly created project directory: cd ratelimit
  6. Install: npm install

Installing Yoga

GraphQL Yoga provides a lightweight, fully-featured JavaScript and TypeScript GraphQL server.

To install GraphQL Yoga and the necessary GraphQL tools:

npm i graphql-yoga graphql

Installing Arcjet

  1. To install the Arcjet JS SDK:

npm i @arcjet/next

  1. Create a free Arcjet account, create a site, and then get the key.
  2. Create a .env.local file in the project’s root. Add the following to this file:

ARCJET_KEY=keyhere

Getting the Arcjet key from the dashboard.

Building A Next.js Application with a Yoga GraphQL Server and Implementing Arcjet Protections

To maintain the reliability and stability of your API, it’s crucial for servers to enforce appropriate API usage limits. Enforcement prevents server overload by managing traffic surges and ensures fair resource distribution among users.

Limits also mitigate abuse and malicious attacks, control costs associated with resource usage and improve performance by preventing any single user from monopolizing server resources.

To impose limits on the calls made to the API endpoint, we will be using the following two Arcjet primitives:

  • Rate limiting: Protects against excessive resource usage, brute force attacks, spraying and fuzzing attacks.
  • Bot protection: Detects and blocks automated clients.

Let's create some example API endpoints:

app/api/graphql/route.ts

Create a /src/app/api/graphql directory and a route.ts file within it:

import { createSchema, createYoga } from 'graphql-yoga';
import arcjet, { detectBot, tokenBucket } from '@arcjet/next';
import { NextResponse } from 'next/server';

// Initialize Arcjet with your configuration, tracking by IP by default.
const aj = arcjet({
  key: process.env.ARCJET_KEY!,
  // No characteristics specified means tracking by IP.
  rules: [
    // Create a token bucket rate limit. Other algorithms are supported.
    tokenBucket({
      mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
      refillRate: 5, // refill 5 tokens per interval
      interval: 10, // refill every 10 seconds
      capacity: 10, // bucket maximum capacity of 10 tokens
    }),
    detectBot({
      mode: "LIVE", // will block requests. Use "DRY_RUN" to log only
      // Block all bots except search engine crawlers. See the full list of bots
      // for other options: https://arcjet.com/bot-list
      allow: ["CATEGORY:SEARCH_ENGINE"],
    }),
  ],
});

// Create GraphQL Yoga instance.
const { handleRequest } = createYoga({
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          "This is the `greetings` field of the root `Query` type",
      },
    },
  }),

  // Configure GraphQL Yoga to use the correct endpoint.
  graphqlEndpoint: "/api/graphql",

  // Yoga needs to know how to create a valid Next response.
  fetchAPI: { Response },
});

export async function GET(req: Request) {
  const decision = await aj.protect(req, { requested: 5 }); // Deduct 5 tokens from the bucket

  console.log("Arcjet decision", decision);

  if (decision.isDenied()) {
    return NextResponse.json(
      { error: "Too Many Requests", reason: decision.reason },
      { status: 429 }
    );
  }

  // Define a context object if needed or use an empty object.
  const ctx = {};

  // Forward the request to the GraphQL handler with both request and context.
  return handleRequest(req, ctx);
}

export async function POST(req: Request) {
  const decision = await aj.protect(req, { requested: 5 }); // Deduct 5 tokens from the bucket

  console.log("Arcjet decision", decision);

  if (decision.isDenied()) {
    return NextResponse.json(
      { error: "Too Many Requests", reason: decision.reason },
      { status: 429 }
    );
  }

  // Define a context object if needed or use an empty object.
  const ctx = {};

  // Forward the request to the GraphQL handler with both request and context
  return handleRequest(req, ctx);
}

export async function OPTIONS(req: Request) {
  // Handle OPTIONS requests here if needed, usually for CORS.
  return NextResponse.json({ message: "OPTIONS request received" });
}

Test the Protections

Now, there is Arcjet rate limiting in place! To test your web application run npm run dev and visit: http://localhost:3000/api/graphql

Send queries in quick succession or repeatedly refresh the page. Per the rule, you were only allowed to make 2 requests because we set the bucket size to 10 and the "cost" per request to 5.

Rate limit response.

To test bot protection, issue a curl request:

curl http://localhost/api/graphql

Bot protection response.

By default, curl is considered an automated client so is blocked.

Requests blocked shown in the Arcjet dashboard.

As this is an API, you probably want to allow requests from tools and programmatic clients. Arcjet supports allowing specific clients as well as categories of clients.

Allow curl

In this case we'll just allow curl to access our API. Change the detectBot rule to the following:

detectBot({
  mode: "LIVE",
  // configured with a list of bots to allow from
  // https://arcjet.com/bot-list - all other detected bots will be blocked
  allow: [
    "CURL", // allows the default user-agent of the `curl` tool
  ],
})

Execute the curl request again and you'll see your request is allowed.

Allow programmatic clients

As this is an API, we also want to allow requests from other programming languages and tools, so we'll use the CATEGORY:PROGRAMMATIC and CATEGORY:TOOL categories:

detectBot({
  mode: "LIVE",
  // configured with a list of bots to allow from
  // https://arcjet.com/bot-list - all other detected bots will be blocked
  allow: [
    "CATEGORY:PROGRAMMATIC",
    "CATEGORY:TOOL", // curl is included in this category
  ],
})

We don't need to include curl because it is already included in the "CATEGORY:TOOL" category.

Additional Protections

In addition to the protection measures provided by Arcjet, you can change the following settings to improve security:

Disable the GraphiQL IDE

By default, the GraphiQL playground interface is only available in development mode and is served at the /graphql route for GET requests with an Accept: text/html header.

To disable it completely, add the graphiql: false line to the Yoga instance:

const { handleRequest } = createYoga({
  graphiql: false,
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          "This is the `greetings` field of the root `Query` type",
      },
    },
  }),

However, disabling the playground should not be considered as a security measure. As long as introspection and/or the suggestion feature are enabled, malicious attackers have the means of enumerating the schema.

Disable Introspection

Schema introspection is used to ask a GraphQL schema for information about what queries/features it supports.

GraphQL introspection.

To disable introspection, first install the plugin: npm i @graphql-yoga/plugin-disable-introspection

Next, import the plugin:

import { useDisableIntrospection } from '@graphql-yoga/plugin-disable-introspection'

Then, add it to the Yoga instance:

const { handleRequest } = createYoga({
  graphiql: false,
  plugins: [useDisableIntrospection()],
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          "This is the `greetings` field of the root `Query` type",
      },
    },
  }),

To enable introspection for certain users, view the Yoga documentation for more details.

GraphQL introspection disabled.

Disable Autocomplete Suggestions

When constructing GraphQL operations, autocomplete suggestions will be made. To disable these suggestions, the graphql-armor plugin suite can be implemented.

To disable introspection, first install the plugin: npm i @escape.tech/graphql-armor-block-field-suggestions

Next import the plugin:

import { blockFieldSuggestionsPlugin } from '@escape.tech/graphql-armor-block-field-suggestions'

Next, add it to the plugin array:

const { handleRequest } = createYoga({
  graphiql: false,
  plugins: [
    useDisableIntrospection(),
    blockFieldSuggestionsPlugin()
  ],
  schema: createSchema({
    typeDefs: /* GraphQL */ `
      type Query {
        greetings: String
      }
    `,
    resolvers: {
      Query: {
        greetings: () =>
          "This is the `greetings` field of the root `Query` type",
      },
    },
  }),

Persisted Operations

To prevent unauthorized actors from reverse-engineering your GraphQL schema and executing arbitrary operations, it is highly recommended to use persisted operations.

Instead of sending full queries with each request, clients use unique identifiers to reference pre-approved and predefined operations stored on the server. This prevents malicious attackers from being able to see or modify the queries - ensuring only safe and trusted queries are executed.

By default the plugin provided by Yoga for persisted operations uses the APQ Specification.

Import the plugin:

import { usePersistedOperations } from '@graphql-yoga/plugin-persisted-operations'

Add it to the Yoga instance along with a stored SHA-256 hash of the query string:

const store = {
  ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38:
    "{__typename}",
};

// Create GraphQL Yoga instance.
const { handleRequest } = createYoga({
  graphiql: false,
  plugins: [
    useDisableIntrospection(),
    blockFieldSuggestionsPlugin(),
    usePersistedOperations({
      getPersistedOperation(sha256Hash: string) {
        return store[sha256Hash];
      },
      extractPersistedOperationId(_params, request) {
        const url = new URL(request.url);
        return url.searchParams.get("id");
      },
    }),
  ],

Now, the hash can be used to reference the operation by providing it as the value to the id query parameter:

http://localhost:3000/api/graphql?id=ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38

Try adding a hash for the {greetings} query string:

const store = {
  ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38:
    "{__typename}",
    bbe53eef47146afab42c34603960ca40be3aa8bfa67a49f3f5f3d6d006df38f0:
    "{greetings}",
};

Now use the hash value representing the {greetings} operation:

http://localhost:3000/api/graphql?id=bbe53eef47146afab42c34603960ca40be3aa8bfa67a49f3f5f3d6d006df38f0

Any operations that are not registered to a hash will not be allowed.

Conclusion

You now have a grasp of integrating Arcjet protection with other security mechanisms to safeguard your GraphQL endpoint.

Additionally, you can enhance protection against API abuse by implementing further measures, such as setting up Cross-Origin Resource Sharing (CORS) rules and applying Cross-Site Request Forgery (CSRF) protections

Related articles

Next.js server action security
Next.js
9 min read

Next.js server action security

Server actions are an elegant way to handle simple functions for common actions like form submissions, but they're a public API so you still need to consider security.

Subscribe by email

Get the full posts by email every week.