rate limiting with @fastify/rate-limit

node 20.13.0 pnpm @fastify/rate-limit ^9.1.0 fastify ^4.27.0 @trpc/server 11.0.0-rc.401 gotta break this up because discord says its too long but let me know if you find this helpful!
// server/index.ts
import fastify from 'fastify';
import fastifyRateLimit from '@fastify/rate-limit';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';

import { appRouter } from './routers/index.js';
import { createContext } from './constants/context.js';

const server = fastify({ logger: { transport: { target: 'pino-pretty' } } });

// ...

await server.register(
fastifyRateLimit,
{ global: false },
);

await server.register(
fastifyTRPCPlugin,
{
prefix: '/trpc',
trpcOptions: {
createContext: createContext.bind(server),
router: appRouter,
},
},
);

// ...
// server/index.ts
import fastify from 'fastify';
import fastifyRateLimit from '@fastify/rate-limit';
import { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';

import { appRouter } from './routers/index.js';
import { createContext } from './constants/context.js';

const server = fastify({ logger: { transport: { target: 'pino-pretty' } } });

// ...

await server.register(
fastifyRateLimit,
{ global: false },
);

await server.register(
fastifyTRPCPlugin,
{
prefix: '/trpc',
trpcOptions: {
createContext: createContext.bind(server),
router: appRouter,
},
},
);

// ...
// server/routers/index.ts
import { inferRouterOutputs } from '@trpc/server';

import {
middleware,
procedure,
router,
} from '../constants/trpc.js';
import rateLimited from '../middleware/rate-limited.js';

export const appRouter = router({
demo: procedure
.use(rateLimited({ max: 1 }))
.query(() => null)
});

export type AppRouter = typeof appRouter;
export type RouterOutputs = inferRouterOutputs<AppRouter>;
// server/routers/index.ts
import { inferRouterOutputs } from '@trpc/server';

import {
middleware,
procedure,
router,
} from '../constants/trpc.js';
import rateLimited from '../middleware/rate-limited.js';

export const appRouter = router({
demo: procedure
.use(rateLimited({ max: 1 }))
.query(() => null)
});

export type AppRouter = typeof appRouter;
export type RouterOutputs = inferRouterOutputs<AppRouter>;
// server/constants/trpc.ts
import { initTRPC } from '@trpc/server';

import type { Context } from './context';

const {
middleware,
procedure,
router,
} = initTRPC.context<Context>().create();

export {
middleware,
procedure,
router,
};
// server/constants/trpc.ts
import { initTRPC } from '@trpc/server';

import type { Context } from './context';

const {
middleware,
procedure,
router,
} = initTRPC.context<Context>().create();

export {
middleware,
procedure,
router,
};
// server/constants/context.ts
import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
import { FastifyInstance } from 'fastify';

export function createContext(
this: FastifyInstance,
{
req,
res,
}: CreateFastifyContextOptions,
) {
return {
app: this,
req,
res,
};
};

export type Context = ReturnType<typeof createContext>;
// server/constants/context.ts
import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
import { FastifyInstance } from 'fastify';

export function createContext(
this: FastifyInstance,
{
req,
res,
}: CreateFastifyContextOptions,
) {
return {
app: this,
req,
res,
};
};

export type Context = ReturnType<typeof createContext>;
1 Reply
theMostCuriousHomunculus
// server/middleware/rate-limited.ts
import { TRPCError } from '@trpc/server';
import type { RateLimitOptions } from '@fastify/rate-limit';

import { middleware } from '../constants/trpc.js';

const rateLimited = (options: RateLimitOptions) => middleware(
async ({
ctx,
next,
}) => {
try {
await new Promise(
(
resolve,
reject,
) => {
ctx
.app
.rateLimit(options)
.bind(ctx.app)(ctx.req, ctx.res)
.then(resolve)
.catch((error) => {
reject(new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: (error as Error).message,
}));
});
},
);

return next();
} catch (error) {
ctx.app.log.error(error);

if (error instanceof TRPCError) throw error;

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: (error as Error).message,
});
}
},
);

export default rateLimited;
// server/middleware/rate-limited.ts
import { TRPCError } from '@trpc/server';
import type { RateLimitOptions } from '@fastify/rate-limit';

import { middleware } from '../constants/trpc.js';

const rateLimited = (options: RateLimitOptions) => middleware(
async ({
ctx,
next,
}) => {
try {
await new Promise(
(
resolve,
reject,
) => {
ctx
.app
.rateLimit(options)
.bind(ctx.app)(ctx.req, ctx.res)
.then(resolve)
.catch((error) => {
reject(new TRPCError({
code: 'TOO_MANY_REQUESTS',
message: (error as Error).message,
}));
});
},
);

return next();
} catch (error) {
ctx.app.log.error(error);

if (error instanceof TRPCError) throw error;

throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: (error as Error).message,
});
}
},
);

export default rateLimited;