> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getsabo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Server APIs

> How to build secure, consistent API routes in Sabo (validation, auth guards, error handling, and best practices).

<Info>
  Use this guide as your blueprint for building API routes under <code>src/app/api/\*</code>. It covers input validation with Zod, optional authentication guards with Supabase, consistent responses, security, and testing. For a concrete, fully implemented example, see <a href="/api-reference/endpoint/contact-api">Contact Form API</a>.
</Info>

<Note>
  To decide between protecting at the edge (middleware) vs inside the route (server guard), see <a href="/core-features/routing-and-middleware">Routing & Middleware</a>. In general, use route-level guards for most APIs, and reserve middleware for global page gating or cross‑cutting concerns.
</Note>

## Route structure

Each endpoint lives in its own folder with a <code>route.ts</code> file:

```ts src/app/api/contact/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

const contactSchema = z.object({
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  email: z.string().email(),
  company: z.string().min(2),
  message: z.string().min(10),
});

export async function POST(request: NextRequest) {
  const body = await request.json();
  const data = contactSchema.parse(body);
  // ... your logic ...
  return NextResponse.json({ success: true });
}
```

<Tip>
  Prefer one responsibility per route. Split complex features into multiple endpoints with clear names.
</Tip>

## Validation with Zod

1. Define a schema near the top of your file:

```ts src/app/api/example/route.ts theme={null}
const schema = z.object({
  name: z.string().min(1, "Name is required"),
});
```

2. Parse and handle validation errors consistently:

```ts src/app/api/contact/route.ts theme={null}
try {
  const body = await request.json();
  const data = contactSchema.parse(body);
  // use `data`
} catch (error) {
  if (error instanceof z.ZodError) {
    return NextResponse.json(
      { success: false, message: "Validation failed", errors: error.issues },
      { status: 400 }
    );
  }
  return NextResponse.json(
    { success: false, message: "An error occurred while processing your request" },
    { status: 500 }
  );
}
```

<Note>
  The error shape above matches the Contact endpoint, keeping your API responses predictable across routes.
</Note>

### Handling non-JSON payloads

Some integrations (Stripe Checkout, webhooks, file uploads) submit data as `application/x-www-form-urlencoded` or `multipart/form-data`. Read those bodies with `request.formData()` before passing values into your schema:

```ts src/app/api/checkout_sessions/route.ts theme={null}
const formData = await request.formData();
const price_id = formData.get("price_id");

if (!price_id || price_id === "undefined") {
  return NextResponse.json(
    { error: "Valid price ID is required" },
    { status: 400 }
  );
}

const lineItems = checkoutSchema.parse({ price_id });
```

<Tip>
  When you only need a few fields from `formData`, pull them out explicitly (as above) and run them through the same Zod schema. This keeps validation identical regardless of transport format.
</Tip>

## Optional: Authentication guard (Supabase)

For private endpoints, use the server client to read the session and return 401 when missing:

```ts src/app/api/private/route.ts theme={null}
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { z } from "zod";

export async function POST(request: Request) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) {
    return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 });
  }

  // ... continue with validation and logic
  return NextResponse.json({ success: true });
}
```

<Warning>
  Avoid leaking sensitive details in error messages. Return generic messages for <code>401</code>/<code>403</code>.
</Warning>

## Consistent responses

* **Success (200/201)**: `{ success: true, ... }`
* **Validation error (400)**: `{ success: false, message: "Validation failed", errors: [...] }`
* **Unauthorized (401/403)**: `{ success: false, message: "Unauthorized" }`
* **Server error (500)**: `{ success: false, message: "An error occurred while processing your request" }`

<Tip>
  Keep the top-level fields stable so clients can handle errors uniformly.
</Tip>

## Security checklist

* Accept only needed methods; return 405 for others.
* Validate and sanitize all inputs (Zod).
* Do not log secrets or PII. Redact when necessary.
* Use auth guards for protected resources.
* Avoid open redirects and untrusted origins.

```ts src/app/api/contact/route.ts theme={null}
export async function GET() {
  return NextResponse.json(
    { success: false, message: "Method not allowed" },
    { status: 405 }
  );
}
```

## Testing locally

```bash theme={null}
# Send a local request
curl -X POST 'http://localhost:3000/api/contact' \
  -H 'Content-Type: application/json' \
  -d '{"firstName":"Jane","lastName":"Doe","email":"jane@example.com","company":"Acme","message":"Hello"}'
```

```json theme={null}
// Expected response
{ "success": true, "message": "Thank you for your message! We'll get back to you soon." }
```

<Note>
  Use <a href="/api-reference/endpoint/contact-api">Contact Form API</a> as a reference implementation for validation, error handling, and method handling.
</Note>
