API

Protected routes

Learn how to protect your API routes.

tRPC has built-in support for middlewares, which are functions that can be used to modify the context or execute code before or after a procedure is executed.

That's how we can secure our API routes from unauthorized access. Below are some examples of you can leverage middlewares to protect your API routes.

Authenticated access

As we're storing user's active auth session in tRPC Context, we can access it in any of our middlewares and procedures. That way we can validate if our API should run the procedure even before its execution.

Here is the example of middleware that validates if the user is currently logged in:

trpc.ts
/**
 * Reusable middleware that enforces users are logged in
 * before running the procedure
 */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user?.id) {
    throw new ApiError(
      HttpStatusCode.UNAUTHORIZED,
      "You need to be logged in to access this feature!",
    );
  }
 
  return next({
    ctx: {
      // infers the `user` as non-nullable
      user: ctx.user,
    },
  });
});

Then we can use our defined middleware with our procedure within its definition:

router.ts
const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
 
export const billingRouter = createTRPCRouter({
  getCustomer: protectedProcedure.input(...).query(...),
});

Feature-based access

When developing your API you may want to restrict access to certain features based on the user's current subscription plan. (e.g. only users with "Pro" plan can access teams).

You can achieve this by creating a middleware that will check if the user has access to the feature and then pass the execution to the next middleware or procedure:

trpc.ts
/**
 * Reusable middleware that enforces users have access to a feature
 * before running the procedure
 */
const enforceFeatuteAvailable = (feature: Feature) =>
  t.middleware(async ({ ctx, next }) => {
    const customerService = customerServiceFactory(ctx.db);
 
    if (!ctx.user?.id) {
      throw new ApiError(
        HttpStatusCode.UNAUTHORIZED,
        "You need to be logged in to access this feature!",
      );
    }
 
    const { data: customer } = await customerService.getCustomerById(
      ctx.user.id,
    );
 
    const hasFeature = isFeatureAvailable(customer, feature);
 
    if (!hasFeature) {
      throw new ApiError(
        HttpStatusCode.PAYMENT_REQUIRED,
        "Upgrade your plan to access this feature!",
      );
    }
 
    return next();
  });

Use it within your procedure the same way as we did with enforceUserIsAuthed middleware:

router.ts
const featuredProcedure = t.procedure.use(enforceFeatuteAvailable);
 
export const teamsRouter = createTRPCRouter({
  getTeams: featuredProcedure(FEATURES.PRO.TEAMS).input(...).query(...),
});

These are just examples of what you can achieve with tRPC middlewares. You can use them to add any kind of logic to your API (e.g. logging, rate limiting, etc.)

Last updated on

On this page

Ship your startup everywhere. In minutes.