lee.david.cs

Rate-Limiting your GraphQL Nexus Resolvers

February 22, 2021

Maintaining a GraphQL server, it is good practice to protect against malicious users who may want to do things such as issuing overly-complex queries, perform a DoS attack or web-scrape.

In this post, we'll build a custom plugin for GraphQL Nexus to rate-limit resolvers to mitigate some threat.

In your GraphQL Nexus server, install the graphql-rate-limit package.

1npm install graphql-rate-limit

We will be using this to build our plugin.

Now create your rate-limit plugin file. And start by defining the typings for your plugin:

1// file: /src/server/graphql/nexus/plugins/rate-limit.plugin
2
3import { GraphQLResolveInfo } from "graphql";
4import { GraphQLRateLimitDirectiveArgs } from "graphql-rate-limit/build/main/lib/types";
5import {
6 ArgsValue,
7 GetGen,
8 MaybePromise,
9 SourceValue
10} from "nexus/dist/core";
11
12// This will be the type of your exposed plugin field in Nexus
13export type FieldRateLimitResolver<TypeName extends string, FieldName extends string> = (
14 root: SourceValue<TypeName>,
15 args: ArgsValue<TypeName, FieldName>,
16 context: GetGen<"context">,
17 info: GraphQLResolveInfo
18) => MaybePromise<GraphQLRateLimitDirectiveArgs>;

Next, to guide Nexus to import the correct types during artifact generation, specify a generated typings import pointing to the type you just created:

1import { oneLine } from "common-tags";
2/* ... */
3import {
4 ArgsValue,
5 GetGen,
6 MaybePromise,
7 printedGenTyping,
8 printedGenTypingImport,
9 SourceValue
10} from "nexus/dist/core";
11
12// This will be the type of your exposed plugin field in Nexus
13export type FieldRateLimitResolver /* ... */;
14
15const fieldRateLimitResolverImport = printedGenTypingImport({
16 module: "@/server/graphql/nexus/plugins/rate-limit.plugin",
17 bindings: ["FieldRateLimitResolver"]
18});
19
20// This will guide Nexus on where to bind the types for your custom plugin
21const fieldDefTypes = printedGenTyping({
22 optional: true,
23 name: "rateLimit",
24 description: oneLine`
25 Rate limit plugin for an individual field. Uses the same directive args as
26 \`graphql-rate-limit\`.
27 `,
28 type: "FieldRateLimitResolver<TypeName, FieldName>",
29 imports: [fieldRateLimitResolverImport]
30});
31
32/* ... */

Then, define the actual logic for your custom plugin:

1/* ... */
2import { GraphQLResolveInfo } from "graphql";
3import { getGraphQLRateLimiter } from "graphql-rate-limit";
4import {
5 GraphQLRateLimitConfig,
6 GraphQLRateLimitDirectiveArgs
7} from "graphql-rate-limit/build/main/lib/types";
8/* ... */
9import { plugin } from "nexus";
10
11/* ... */
12
13export interface RateLimitPluginConfig extends GraphQLRateLimitConfig {
14 defaultRateLimit?: GraphQLRateLimitDirectiveArgs;
15}
16
17/* Define the actual plugin to be used with your Nexus Schema */
18export const rateLimitPlugin = (options: RateLimitPluginConfig) => {
19 const rateLimiter = getGraphQLRateLimiter(options);
20
21 return plugin({
22 name: "CustomNexusRateLimit",
23 description: "The rateLimit plugin provides field-level rate limiting for a schema",
24 fieldDefTypes,
25 onCreateFieldResolver: (config) => {
26 const rateLimit = config.fieldConfig.extensions?.nexus?.config.rateLimit;
27
28 /**
29 * @description If the field doesn't have a rateLimit field, and no top-level default
30 * was configured on the schema, don't worry about wrapping the resolver
31 */
32 if (rateLimit == null && !options.defaultRateLimit) {
33 return;
34 }
35
36 return async (parent, args, context, info, next) => {
37 const rateLimitArgs: GraphQLRateLimitDirectiveArgs =
38 rateLimit?.(parent, args, context, info) ?? options.defaultRateLimit!;
39
40 const errorMessage: Maybe<string> = await rateLimiter(
41 { parent, args, context, info },
42 rateLimitArgs
43 );
44
45 if (errorMessage) {
46 throw new Error(errorMessage);
47 }
48
49 return next(parent, args, context, info);
50 };
51 }
52 });
53};

Lastly, apply it to your Nexus Schema

1import { GraphQLServerContext } from "./context";
2import { rateLimitPlugin } from "./plugins";
3import { GraphQLSchema } from "graphql";
4import { RedisStore } from "graphql-rate-limit";
5import { makeSchema } from "nexus";
6/* ... */
7
8export const schema: GraphQLSchema = makeSchema({
9 /* ... */
10 plugins: [
11 /* ... */
12 rateLimitPlugin({
13 idenfityContext: ({ user, req }: GraphQLServerContext): string => {
14 const userId: Maybe<string> = user?.id;
15 const ip: Maybe<string> = getClientIp(req);
16
17 const identityKey: string = userId ?? ip ?? "";
18
19 return identityKey;
20 },
21 // Optional, if you don't want to use a InMemoryStore
22 store: new RedisStore(redis.instance)
23 })
24 /* ... */
25 ],
26 /* ... */
27});

Nice! Now after generating your GraphQL Nexus type artifacts, you can access a rateLimit field to rate limit your field resolvers.

1/* ... */
2export const users = queryField("users", {
3 type: nonNull(list(nonNull("User"))),
4 // Now you will have access to this typed property
5 // This will rate limit the users query to a max of 30 times in 1 minute per requester
6 rateLimit: () => ({ max: 30, window: "1m" }),
7 args: {
8 where: arg({ type: "UserWhereInput" })
9 },
10 resolve: async (parent, args, ctx, info) => {
11 const { where } = args;
12 const { prisma } = ctx;
13
14 return await prisma.users.findMany({ where });
15 }
16});
17/* ... */

You can take custom Nexus plugins further to easily create things such as yup input validation and graphql-query-complexity on a per-resolver basis!

Happy coding! -- David Lee