# API Routes

> Create and work with Next.js API routes in src/pages/api/. Use when creating API endpoints, handling HTTP requests, or working with server-side API logic in Pages Router. Use when this capability is needed.

- Skill: `tomevault-io/api-routes-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/api-routes-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/api-routes-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/api-routes-2

---


# Next.js API Routes (Pages Router)

## Structure

API routes go in `src/pages/api/` and automatically become endpoints.

- `src/pages/api/users.ts` → `/api/users`
- `src/pages/api/users/[id].ts` → `/api/users/[id]`
- `src/pages/api/posts/[id]/comments.ts` → `/api/posts/[id]/comments`

## Basic API Route

Always use the `HttpResponses` type from `src/types/http-responses.ts` for consistent API responses.

```tsx
// src/pages/api/hello.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type User = {
  name: string;
};

type Response = HttpResponses<User>;

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  if (req.method !== "GET") {
    return res.status(405).json({
      message: HTTP_RESPONSE_MESSAGE.METHOD_NOT_ALLOWED,
      status: 405,
      errors: `Method ${req.method} not allowed`,
    });
  }

  res.status(200).json({
    message: HTTP_RESPONSE_MESSAGE.SUCCESS,
    data: { name: "John Doe" },
    status: 200,
  });
}
```

## HTTP Methods

Handle different HTTP methods using `HttpResponses` type:

```tsx
// src/pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type User = {
  id: string;
  name: string;
  email: string;
};

type Response = HttpResponses<User[] | User>;

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  if (req.method === "GET") {
    // Fetch users
    const users: User[] = [];
    res.status(200).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: users,
      status: 200,
    });
  } else if (req.method === "POST") {
    // Create user
    const { name, email } = req.body;
    
    if (!name || !email) {
      return res.status(400).json({
        message: HTTP_RESPONSE_MESSAGE.BAD_REQUEST,
        status: 400,
        errors: "Name and email are required",
      });
    }
    
    const user: User = { id: Date.now().toString(), name, email };
    res.status(201).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: user,
      status: 201,
    });
  } else {
    res.setHeader("Allow", ["GET", "POST"]);
    res.status(405).json({
      message: HTTP_RESPONSE_MESSAGE.METHOD_NOT_ALLOWED,
      status: 405,
      errors: `Method ${req.method} not allowed`,
    });
  }
}
```

## Dynamic Routes

```tsx
// src/pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type User = {
  id: string;
  name: string;
  email: string;
};

type Response = HttpResponses<User>;

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  const { id } = req.query;
  
  if (req.method === "GET") {
    // Fetch user by id
    if (!id || Array.isArray(id)) {
      return res.status(400).json({
        message: HTTP_RESPONSE_MESSAGE.BAD_REQUEST,
        status: 400,
        errors: "Invalid user ID",
      });
    }
    
    const user: User = { id, name: "User", email: "user@example.com" };
    res.status(200).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: user,
      status: 200,
    });
  } else if (req.method === "PUT") {
    // Update user
    const updatedUser: User = { id: id as string, ...req.body };
    res.status(200).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: updatedUser,
      status: 200,
    });
  } else if (req.method === "DELETE") {
    // Delete user
    res.status(204).end();
  } else {
    res.setHeader("Allow", ["GET", "PUT", "DELETE"]);
    res.status(405).json({
      message: HTTP_RESPONSE_MESSAGE.METHOD_NOT_ALLOWED,
      status: 405,
      errors: `Method ${req.method} not allowed`,
    });
  }
}
```

## Request Body

```tsx
// src/pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type User = {
  id: string;
  name: string;
  email: string;
};

type Response = HttpResponses<User>;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  if (req.method === "POST") {
    const { name, email } = req.body;
    
    // Validate
    if (!name || !email) {
      return res.status(400).json({
        message: HTTP_RESPONSE_MESSAGE.BAD_REQUEST,
        status: 400,
        errors: "Name and email are required",
      });
    }
    
    // Process
    const user: User = { id: Date.now().toString(), name, email };
    
    res.status(201).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: user,
      status: 201,
    });
  }
}
```

## Query Parameters

```tsx
// src/pages/api/search.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type SearchResult = {
  id: string;
  title: string;
};

type Response = HttpResponses<{ results: SearchResult[]; limit: number }>;

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  const { q, limit = "10" } = req.query;
  
  if (!q || Array.isArray(q)) {
    return res.status(400).json({
      message: HTTP_RESPONSE_MESSAGE.BAD_REQUEST,
      status: 400,
      errors: "Query parameter 'q' is required",
    });
  }
  
  // Search logic
  const results: SearchResult[] = [];
  
  res.status(200).json({
    message: HTTP_RESPONSE_MESSAGE.SUCCESS,
    data: { results, limit: Number(limit) },
    status: 200,
  });
}
```

## Error Handling

```tsx
// src/pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { HTTP_RESPONSE_MESSAGE, type HttpResponses } from "@/types/http-responses";

type User = {
  id: string;
  name: string;
  email: string;
};

type Response = HttpResponses<User>;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Response["success"]["data"] | Response["error"]>,
) {
  try {
    const { id } = req.query;
    
    if (!id || Array.isArray(id)) {
      return res.status(400).json({
        message: HTTP_RESPONSE_MESSAGE.BAD_REQUEST,
        status: 400,
        errors: "ID is required",
      });
    }
    
    // Fetch user
    const user = await fetchUser(id);
    
    if (!user) {
      return res.status(404).json({
        message: HTTP_RESPONSE_MESSAGE.NOT_FOUND,
        status: 404,
        errors: "User not found",
      });
    }
    
    res.status(200).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: user,
      status: 200,
    });
  } catch (error) {
    console.error(error);
    res.status(500).json({
      message: HTTP_RESPONSE_MESSAGE.INTERNAL_SERVER_ERROR,
      status: 500,
      errors: "Internal server error",
    });
  }
}
```

## Data Transformations (External APIs)

When working with external APIs, use normalizers and serializers to transform data (only when the route calls an external API):

```tsx
// src/pages/api/users.ts
import { normalizeUser, normalizeUserList } from "@/lib/normalizers";
import { serializeCreateUser } from "@/lib/serializers";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "GET") {
    const response = await fetch(`${process.env.EXTERNAL_API_URL}/users`);
    const externalData = await response.json();
    
    // Normalize: External (verbose) → Internal (short)
    const users = normalizeUserList(externalData.data);
    
    return res.status(200).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: { users },
      status: 200,
    });
  }
  
  if (req.method === "POST") {
    const body = req.body as CreateUserBody;
    
    // Serialize: Internal (short) → External (verbose)
    const serializedData = serializeCreateUser(body);
    
    const response = await fetch(`${process.env.EXTERNAL_API_URL}/users`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(serializedData),
    });
    
    const externalData = await response.json();
    const user = normalizeUser(externalData);
    
    return res.status(201).json({
      message: HTTP_RESPONSE_MESSAGE.SUCCESS,
      data: { user },
      status: 201,
    });
  }
}
```

**Note**: External types (e.g. `ExternalUsersResponse`) and internal types (e.g. `User`) live in `src/types/`. Use `externalClient` from `@/lib/api/external-client` when proxying to external APIs. See `src/lib/TRANSFORMATIONS.md` for field mappings (if your project uses it).

## Important Notes

- **Callers**: These routes are called by **actions** in `src/lib/api/{resource}/actions.ts`. The app flow is: Page → Hook → Action → this API route → (optional) external API. Keep response shape consistent with what actions expect (`res.json({ message, data, status })` with `data` as the payload).
- **Always use `HttpResponses` type** from `src/types/http-responses.ts` for consistent API responses
- Import `HTTP_RESPONSE_MESSAGE` enum for standardized error messages
- **Use normalizers** only when this route calls an external API (when receiving external data)
- **Use serializers** only when this route sends data to an external API
- See `src/lib/TRANSFORMATIONS.md` for field mappings and flow
- API routes are server-side only
- Use `NextApiRequest` and `NextApiResponse` types
- Export a default function named `handler`
- Access dynamic route params via `req.query` (always check if it's an array)
- Access request body via `req.body` (automatically parsed for JSON)
- Use `HttpResponses<T>` where `T` is your data type
- Response structure: `{ message, data, status }` for success, `{ message, status, errors }` for errors
- Set status codes: `res.status(200)`, `res.status(404)`, etc.
- End responses: `res.json()`, `res.end()`, or `res.status(204).end()`
- Check `req.method` to handle different HTTP verbs
- Query params are always strings or string arrays (convert with `Number()`, check with `Array.isArray()`, etc.)

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/jorggerojas) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-13 -->

