Developer/API Routes

API Routes

Type-safe API route handlers with TanStack Start and error handling

API routes use TanStack Router server handlers and handleApiError. Authenticate inline in the handler, then call Convex through the typed helpers when route work needs app data.

API Route Pattern

import { handleApiError } from "@/lib/api-middleware";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/example")({
  server: {
    handlers: {
      GET: async ({ request }) => {
        try {
          return Response.json({ success: true });
        } catch (e) {
          return handleApiError(e);
        }
      },
    },
  },
});

Authentication

import { handleApiError } from "@/lib/api-middleware";
import { getRequiredUser } from "@/lib/auth/auth-user";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/protected")({
  server: {
    handlers: {
      GET: async () => {
        try {
          const user = await getRequiredUser();
          return Response.json({ userId: user.id });
        } catch (e) {
          return handleApiError(e);
        }
      },
    },
  },
});

Convex Calls

import { handleApiError } from "@/lib/api-middleware";
import { getRequiredUser, isAdmin } from "@/lib/auth/auth-user";
import { fetchAuthQuery } from "@/lib/auth-server";
import { HttpError } from "@/lib/errors/http-error";
import { api } from "@convex/_generated/api";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/admin/users")({
  server: {
    handlers: {
      GET: async () => {
        try {
          const user = await getRequiredUser();
          if (!isAdmin(user)) throw new HttpError("Forbidden", 403);

          const users = await fetchAuthQuery(api.admin.queries.listUsers, {
            page: 1,
            pageSize: 20,
          });
          return Response.json(users);
        } catch (e) {
          return handleApiError(e);
        }
      },
    },
  },
});

Error Handling

Error TypeStatusBehavior
HttpErrorCustomReturns error message with specified status
ApplicationError400Returns error message
ZodError422Returns validation error details
Unknown Error (dev)500Returns full error message
Unknown Error (prod)500Returns "Internal server error"
import { HttpError } from "@/lib/errors/http-error";
import { ApplicationError } from "@/lib/errors/application-error";

throw new HttpError("Not found", 404);
throw new HttpError("Forbidden", 403);
throw new ApplicationError("Invalid operation");