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:
Create a directory in which you would like to store the application: mkdir arcjet
Enter the newly created directory: cd arcjet
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 yto proceed.
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
Navigate into the newly created project directory: cd ratelimit
Install: npm install
Installing Yoga
GraphQL Yoga provides a lightweight, fully-featured JavaScript and TypeScript GraphQL server.
To install GraphQL Yogaand the necessary GraphQLtools:
Create a .env.local file in the project’s root. Add the following to this file:
ARCJET_KEY=keyhere
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.
Create a /src/app/api/graphql directory and aroute.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.
To test bot protection, issue a curl request:
curl http://localhost/api/graphql
By default, curl is considered an automated client so is blocked.
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:TOOLcategories:
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.
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.
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:
Nosecone is an open source library to set security headers like Content Security Policy (CSP) and HTTP Strict Transport Security (HSTS) on Next.js, SvelteKit, and other JavaScript frameworks using Bun, Deno, or Node.js. Security headers as code.
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.