_private/qwestly-docs/Engineering/API-Routes-Guide.md

API Routes Guide

NextJS provides a flexible way to create API endpoints using the App Router through route handlers. These are defined in your app/api directory and allow you to create serverless functions that can handle HTTP requests.

Basic API Route

The most basic API route is created by adding a route.ts file in your app/api directory:

// app/api/hello/route.ts
import { NextResponse } from 'next/server'
 
export async function GET() {
  return NextResponse.json({ message: 'Hello World' })
}

Dynamic Routes with Parameters

Single Dynamic Parameter [id]

You can capture dynamic parameters using square brackets [id] in the folder name:

// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const id = params.id
  return NextResponse.json({ postId: id })
}

This will match routes like:

  • /api/posts/1
  • /api/posts/2
  • /api/posts/any-string

Catch-all Routes

Using double brackets [[...slug]] makes the catch-all parameter optional:

// app/api/posts/[[...slug]]/route.ts
import { NextResponse } from 'next/server'

export async function GET(
  request: Request,
  { params }: { params: { slug?: string[] } }
) {
  const slug = params.slug || []
  return NextResponse.json({ path: slug })
}

This will match:

  • /api/postsslug = []
  • /api/posts/aslug = ['a']
  • /api/posts/a/bslug = ['a', 'b']

Using ApiRouter for Advanced Route Management

The ApiRouter class provides a more sophisticated and FastAPI-inspired approach to handling API routes in NextJS. It offers features like type-safe route parameters, search parameter handling, and optional authentication checks.

Basic Setup

Create a catch-all route file (e.g., app/api/users/[[...slug]]/route.ts) and initialize the router:

import ApiRouter from "@/app/utils/api-router";

const router = new ApiRouter({
  // Optional: Add authentication check for auth routes
  authenticate: async (request) => {
    return true; // Your auth logic here
  },
  // Optional: boolean to authenticate all routes
  authAllRoutes: true,
});

// Define your routes; route is relative to /api/users
router.get(route, handler, { auth: boolean });

// Matches /api/users/:userId
router.get("{userId}", async (req, { userId, queryParam }) => {
  // userId is a route parameter
  // queryParam would come from search params
  return NextResponse.json({ userId, queryParam });
});

// matches /api/users
// the slash is optional in case it's weird to route an empty string
router.post("/", async (req) => {
  const body = await req.json();
  return NextResponse.json(body);
});

// Export the route handlers
export const GET = router.getMethodHandler("GET");
export const POST = router.getMethodHandler("POST");

// Alternatively, export all methods at once:
const routes = router.exportRoutes();
export const { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } = routes;

Key Features

  1. Type-Safe Route Parameters

    • Use curly braces for route parameters: users/{userId}
    • Parameters are automatically extracted and typed
    • Supports both route parameters and search parameters
  2. Method-Specific Routes

    router.get("path", handler);    // GET requests
    router.post("path", handler);   // POST requests
    router.put("path", handler);    // PUT requests
    router.delete("path", handler); // DELETE requests
    router.patch("path", handler);  // PATCH requests
    
  3. Authentication Support

    const router = new ApiRouter({
      authenticate: async (request) => {
        // Return true if authenticated, false otherwise
        return verifyAuth(request);
      },
    });
    
    // This route will be authenticated
    router.get("{id}/secret", async (req, { id }) => {
      return NextResponse.json({ id });
    }, { auth: true });
    
  4. Automatic Parameter Handling

    router.get("users/{userId}/posts/{postId}", async (req, params) => {
      const { userId, postId, page, limit } = params;
      // userId and postId are route parameters
      // page and limit would be from ?page=1&limit=10
      return NextResponse.json({ userId, postId, page, limit });
    });
    

Best Practices

  1. Organize Routes by Resource

    // app/api/users/[[...slug]]/route.ts
    router.get("", listUsers);              // GET /api/users
    router.get("{userId}", getUser);        // GET /api/users/123
    router.post("", createUser);            // POST /api/users
    router.put("{userId}", updateUser);     // PUT /api/users/123
    router.delete("{userId}", deleteUser);  // DELETE /api/users/123
    
  2. Handle Errors Consistently

    router.get("{userId}", async (req, { userId }) => {
      try {
        const user = await getUser(userId);
        if (!user) {
          return NextResponse.json(
            { error: "User not found" },
            { status: 404 }
          );
        }
        return NextResponse.json(user);
      } catch (error) {
        return NextResponse.json(
          { error: "Internal server error" },
          { status: 500 }
        );
      }
    });
    
  3. Leverage TypeScript for Better Type Safety

    interface User {
      id: string;
      name: string;
    }
    
    router.post<User>("users", async (req) => {
      const user: User = await req.json();
      return NextResponse.json(user);
    });
    

The ApiRouter provides a more structured and maintainable way to handle API routes in your NextJS application, especially for larger applications with many endpoints. It combines the flexibility of NextJS's routing system with the convenience of a FastAPI-style router.

HTTP Methods

Route handlers support all HTTP methods:

export async function GET(request: Request) { }
export async function POST(request: Request) { }
export async function PUT(request: Request) { }
export async function PATCH(request: Request) { }
export async function DELETE(request: Request) { }
export async function HEAD(request: Request) { }
export async function OPTIONS(request: Request) { }

Response Helpers

NextJS provides several response helpers:

// JSON Response
return NextResponse.json({ data: 'hello' })

// Redirect
return NextResponse.redirect(new URL('/new-page', request.url))

// Rewrite
return NextResponse.rewrite(new URL('/rewritten-page', request.url))

Error Handling

You can handle errors by throwing responses:

import { NextResponse } from 'next/server'

export async function GET() {
  const error = new Error('Not Found')
  error.status = 404
  throw error
  
  // Or use NextResponse
  return NextResponse.json(
    { error: 'Not Found' },
    { status: 404 }
  )
}

Best Practices

  1. Always validate input parameters
  2. Use appropriate HTTP status codes
  3. Handle errors gracefully
  4. Use TypeScript for better type safety
  5. Keep route handlers focused and modular
  6. Use middleware for common operations like authentication
  7. Consider rate limiting for public APIs