# How to Define POST Requests in 9router API Routes

> Easily define POST requests in 9router API routes. Learn to parse request bodies and return JSON responses efficiently using async POST functions and NextResponse.json.

- Repository: [decolua/9router](https://github.com/decolua/9router)
- Tags: how-to-guide
- Published: 2026-05-08

---

**In 9router, POST endpoints are defined by exporting an async `POST` function from a [`route.js`](https://github.com/decolua/9router/blob/main/route.js) file inside `src/app/api/{endpoint}/`, parsing the request body with `await request.json()`, and returning JSON responses via `NextResponse.json()` from `next/server`.**

The decolua/9router repository implements a Next.js 13+ App Router architecture where API routes follow a convention-based file system. Each folder under `src/app/api` becomes an endpoint path, and HTTP methods are exported as async functions that handle incoming requests. To define POST requests in 9router, you create a [`route.js`](https://github.com/decolua/9router/blob/main/route.js) file that exports a `POST` handler, enabling you to build RESTful endpoints for creating providers, handling translations, and managing OAuth flows.

## The 9router POST Route Convention

9router follows the Next.js App Router pattern where file paths automatically define URL routes. To create a POST endpoint, you place a [`route.js`](https://github.com/decolua/9router/blob/main/route.js) (or [`route.ts`](https://github.com/decolua/9router/blob/main/route.ts)) file inside a folder hierarchy under `src/app/api`.

The folder structure directly maps to the endpoint URL. For example, [`src/app/api/providers/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/providers/route.js) handles requests at `/api/providers`. Dynamic segments use bracket notation, such as `src/app/api/providers/[id]/route.js`, which captures the ID as a URL parameter.

Inside the route file, you export an async function named exactly `POST`. Next.js dynamically wires these exports based on the HTTP verb, so only exported methods are active for that endpoint.

```javascript
// src/app/api/example/route.js
import { NextResponse } from "next/server";

export async function POST(request) {
  // Handler implementation
}

```

## Basic POST Endpoint: Echo Pattern

A minimal POST route in 9router parses the incoming JSON body and returns a response. The following example from the codebase demonstrates the essential pattern used across the application, including validation and error handling.

Create [`src/app/api/echo/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/echo/route.js):

```javascript
import { NextResponse } from "next/server";

export async function POST(request) {
  try {
    const payload = await request.json();
    if (!payload?.message) {
      return NextResponse.json(
        { error: "Missing `message` field" },
        { status: 400 }
      );
    }
    
    const result = {
      echo: payload.message,
      receivedAt: new Date().toISOString(),
    };
    
    return NextResponse.json({ success: true, result });
  } catch (e) {
    console.error("Echo POST error:", e);
    return NextResponse.json(
      { error: "Invalid JSON body" },
      { status: 400 }
    );
  }
}

```

This endpoint accepts `POST /api/echo` with a JSON body, validates the presence of a message field, and returns the echoed data with a timestamp.

## Production Patterns from the 9router Codebase

The 9router source code contains several sophisticated POST handlers that demonstrate production-ready patterns for validation, database interaction, and external API calls.

### Creating Provider Connections

The [`src/app/api/providers/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/providers/route.js) file implements a full-featured POST handler for creating new provider connections. It demonstrates input validation, business logic separation, and secure response handling.

```javascript
import { NextResponse } from "next/server";
import {
  createProviderConnection,
} from "@/models";
import { APIKEY_PROVIDERS, WEB_COOKIE_PROVIDERS } from "@/shared/constants/config";

export async function POST(request) {
  try {
    const { provider, apiKey, name } = await request.json();

    const isWebCookie = !!WEB_COOKIE_PROVIDERS[provider];
    const valid = APIKEY_PROVIDERS[provider] || isWebCookie;
    
    if (!provider || !valid) {
      return NextResponse.json(
        { error: "Invalid provider" },
        { status: 400 }
      );
    }
    
    if (!apiKey && !isWebCookie) {
      return NextResponse.json(
        { error: "API key required" },
        { status: 400 }
      );
    }
    
    if (!name) {
      return NextResponse.json(
        { error: "Name required" },
        { status: 400 }
      );
    }

    const connection = await createProviderConnection({
      provider,
      authType: isWebCookie ? "cookie" : "apikey",
      name,
      apiKey: apiKey || "",
      isActive: true,
    });

    const result = { ...connection };
    delete result.apiKey;

    return NextResponse.json({ connection: result }, { status: 201 });
  } catch (e) {
    console.error("Provider POST error:", e);
    return NextResponse.json(
      { error: "Failed to create provider" },
      { status: 500 }
    );
  }
}

```

This handler validates the provider against allowed constants, checks authentication requirements, calls the `createProviderConnection` model helper (which wraps Prisma operations), and strips sensitive fields like `apiKey` before returning the response.

### Multi-Step Translation Pipeline

The [`src/app/api/translator/translate/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/translator/translate/route.js) demonstrates a complex POST handler that processes translation requests in multiple steps. It uses helper functions like `parseModel` and `translateRequest` to transform data between formats, and implements the **executor pattern** to abstract provider-specific logic.

```javascript
import { NextResponse } from "next/server";
import { parseModel } from "open-sse/services/model.js";
import { translateRequest } from "open-sse/translator/index.js";
import { FORMATS } from "open-sse/translator/formats.js";
import { getProviderConnections } from "@/lib/localDb.js";
import { getExecutor } from "open-sse/executors/index.js";

export async function POST(request) {
  try {
    const { step, body } = await request.json();

    if (!step || !body) {
      return NextResponse.json(
        { success: false, error: "step and body required" },
        { status: 400 }
      );
    }

    if (step === 1) {
      const clientBody = body.body || body;
      const { provider, model } = parseModel(clientBody.model);
      const sourceFormat = detectFormat(clientBody);
      const targetFormat = getTargetFormat(provider);
      
      return NextResponse.json({
        success: true,
        result: { provider, model, sourceFormat, targetFormat },
      });
    }

    if (step === 2) {
      const clientBody = body.body || body;
      const { provider, model } = parseModel(clientBody.model);
      const sourceFormat = detectFormat(clientBody);
      const stream = clientBody.stream !== false;

      const interim = translateRequest(
        sourceFormat,
        FORMATS.OPENAI,
        model,
        clientBody,
        stream,
        null,
        provider
      );
      delete interim._toolNameMap;
      
      return NextResponse.json({ success: true, result: { body: interim } });
    }

    if (step === 3) {
      const openaiBody = body.body || body;
      const { provider, model } = body;
      const targetFormat = getTargetFormat(provider);
      const stream = openaiBody.stream !== false;

      const translated = translateRequest(
        FORMATS.OPENAI,
        targetFormat,
        model,
        openaiBody,
        stream,
        null,
        provider
      );
      delete translated._toolNameMap;

      const connections = await getProviderConnections({ provider });
      const connection = connections.find((c) => c.isActive !== false);
      
      if (!connection) {
        return NextResponse.json(
          { success: false, error: `No active connection for ${provider}` },
          { status: 400 }
        );
      }

      const credentials = {
        apiKey: connection.apiKey,
        accessToken: connection.accessToken,
        providerSpecificData: connection.providerSpecificData,
      };

      const executor = getExecutor(provider);
      const url = executor.buildUrl(model, stream, 0, credentials);
      const headers = executor.buildHeaders(credentials, stream);
      const finalBody = executor.transformRequest(
        model,
        translated,
        stream,
        credentials
      );

      return NextResponse.json({
        success: true,
        result: { url, headers, body: finalBody },
      });
    }

    return NextResponse.json(
      { success: false, error: "Invalid step" },
      { status: 400 }
    );
  } catch (e) {
    console.error("Translator POST error:", e);
    return NextResponse.json(
      { success: false, error: e.message },
      { status: 500 }
    );
  }
}

```

This pattern separates concerns by using `getExecutor(provider)` to obtain provider-specific logic for building URLs and headers, while the route handler remains generic and focused on orchestration.

### External API Integration with Cookie Handling

The [`src/app/api/oauth/iflow/cookie/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/oauth/iflow/cookie/route.js) file shows how to handle external API calls within POST handlers, including **cookie normalization** and persistence. This pattern is essential when proxying requests to third-party services, normalizing user data before sending it to external APIs, and storing the resulting connection details in the database.

## Handling Request Bodies and Responses

Successful POST routes in 9router follow consistent patterns for parsing input and generating output.

- **Request Body Parsing:** Use `await request.json()` to parse JSON payloads. For form data, use `request.formData()`. Malformed JSON automatically triggers errors that should be caught in your try/catch block.
- **Response Construction:** Always use `NextResponse.json()` from `next/server` rather than the standard `Response` constructor. This ensures proper content-type headers and automatic serialization.
- **Error Handling:** Wrap all logic in `try/catch` blocks. Log errors server-side using `console.error()`, but return sanitized messages to clients with appropriate HTTP status codes (400 for client errors, 500 for server errors).
- **Database Decoupling:** Following the pattern seen in [`src/app/api/providers/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/providers/route.js), delegate database operations to model helper functions like `createProviderConnection` or `getProviderConnections`. These act as thin wrappers around your ORM (Prisma), keeping routes clean and testable.

## Summary

- **File Location:** Create [`route.js`](https://github.com/decolua/9router/blob/main/route.js) files inside `src/app/api/{path}/` folders, where the folder structure defines the URL endpoint.
- **Export Signature:** Define POST handlers by exporting an async function named exactly `POST(request)` that receives the Request object.
- **Body Parsing:** Extract JSON payloads using `await request.json()` inside the handler, with validation to ensure required fields exist.
- **Response Pattern:** Return data using `NextResponse.json(payload, { status })` from `next/server` to ensure proper headers and status codes.
- **Error Strategy:** Implement comprehensive `try/catch` blocks, log detailed errors server-side, and return safe error messages to clients.
- **Architecture:** Decouple routes from persistence layers by using model helper functions, and utilize the executor pattern for provider-specific logic abstraction.

## Frequently Asked Questions

### Where should I place POST route files in a 9router project?

Place your POST route files inside the `src/app/api` directory using the App Router convention. Create a folder for your endpoint (e.g., `providers`) and add a [`route.js`](https://github.com/decolua/9router/blob/main/route.js) file inside it. For dynamic routes, use brackets like `[id]` to capture URL parameters. The file system path directly maps to the API URL, so [`src/app/api/providers/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/providers/route.js) handles `POST /api/providers`.

### How do I access the request body in a 9router POST handler?

Inside your exported `POST` function, call `await request.json()` to parse JSON bodies. For other content types, use `request.text()` or `request.formData()`. Always validate the parsed data before processing, and wrap the parsing logic in a try/catch block to handle malformed JSON gracefully by returning a 400 status response.

### What is the recommended way to handle errors in 9router API routes?

Wrap your entire handler implementation in a `try/catch` block. Log the detailed error using `console.error()` for server-side debugging, then return a JSON response using `NextResponse.json({ error: "message" }, { status: 500 })` for server errors or status 400 for client errors. This pattern, demonstrated in [`src/app/api/providers/route.js`](https://github.com/decolua/9router/blob/main/src/app/api/providers/route.js), ensures the API remains stable and returns predictable error formats to clients.

### Can I use dynamic routes with POST handlers in 9router?

Yes, dynamic routes work with POST handlers by creating folders with bracket notation (e.g., `[id]`) inside your API directory. For example, `src/app/api/providers/[id]/route.js` handles POST requests to `/api/providers/123`, where the ID becomes a URL parameter accessible within your handler.