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.plugin23import { GraphQLResolveInfo } from "graphql";4import { GraphQLRateLimitDirectiveArgs } from "graphql-rate-limit/build/main/lib/types";5import {6 ArgsValue,7 GetGen,8 MaybePromise,9 SourceValue10} from "nexus/dist/core";1112// This will be the type of your exposed plugin field in Nexus13export type FieldRateLimitResolver<TypeName extends string, FieldName extends string> = (14 root: SourceValue<TypeName>,15 args: ArgsValue<TypeName, FieldName>,16 context: GetGen<"context">,17 info: GraphQLResolveInfo18) => 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 SourceValue10} from "nexus/dist/core";1112// This will be the type of your exposed plugin field in Nexus13export type FieldRateLimitResolver /* ... */;1415const fieldRateLimitResolverImport = printedGenTypingImport({16 module: "@/server/graphql/nexus/plugins/rate-limit.plugin",17 bindings: ["FieldRateLimitResolver"]18});1920// This will guide Nexus on where to bind the types for your custom plugin21const fieldDefTypes = printedGenTyping({22 optional: true,23 name: "rateLimit",24 description: oneLine`25 Rate limit plugin for an individual field. Uses the same directive args as26 \`graphql-rate-limit\`.27 `,28 type: "FieldRateLimitResolver<TypeName, FieldName>",29 imports: [fieldRateLimitResolverImport]30});3132/* ... */
Then, define the actual logic for your custom plugin:
1/* ... */2import { GraphQLResolveInfo } from "graphql";3import { getGraphQLRateLimiter } from "graphql-rate-limit";4import {5 GraphQLRateLimitConfig,6 GraphQLRateLimitDirectiveArgs7} from "graphql-rate-limit/build/main/lib/types";8/* ... */9import { plugin } from "nexus";1011/* ... */1213export interface RateLimitPluginConfig extends GraphQLRateLimitConfig {14 defaultRateLimit?: GraphQLRateLimitDirectiveArgs;15}1617/* Define the actual plugin to be used with your Nexus Schema */18export const rateLimitPlugin = (options: RateLimitPluginConfig) => {19 const rateLimiter = getGraphQLRateLimiter(options);2021 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;2728 /**29 * @description If the field doesn't have a rateLimit field, and no top-level default30 * was configured on the schema, don't worry about wrapping the resolver31 */32 if (rateLimit == null && !options.defaultRateLimit) {33 return;34 }3536 return async (parent, args, context, info, next) => {37 const rateLimitArgs: GraphQLRateLimitDirectiveArgs =38 rateLimit?.(parent, args, context, info) ?? options.defaultRateLimit!;3940 const errorMessage: Maybe<string> = await rateLimiter(41 { parent, args, context, info },42 rateLimitArgs43 );4445 if (errorMessage) {46 throw new Error(errorMessage);47 }4849 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/* ... */78export 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);1617 const identityKey: string = userId ?? ip ?? "";1819 return identityKey;20 },21 // Optional, if you don't want to use a InMemoryStore22 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 property5 // This will rate limit the users query to a max of 30 times in 1 minute per requester6 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;1314 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