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

# Framework Guide: App Router

> Route Handlers, Server Actions and how to keep the API key out of the client.

# Framework Guide: App Router

`@markpdf/nextjs` covers the two common App Router patterns for exposing document conversion to your UI: **Route Handlers** (when you want your own HTTP endpoint, for example to call it from `fetch` on the client or from another service) and **Server Actions** (when the form and server logic live in the same file, without defining a route).

## Rule of thumb: the API key never leaves the server

The entire package assumes that it is imported only from:

* Route Handlers (`app/**/route.ts`)
* Server Actions (`"use server"`)
* Server Components

<Warning>
  If you import `@markpdf/nextjs` from a Client Component (`"use client"`), the bundler will fail or, worse, the final bundle would end up needing the key in the browser. The package is intended not to work outside of the Next.js server environment.
</Warning>

## Route Handler: `createConvertRouteHandler`

```ts app/api/convert/route.ts theme={null}
import { createConvertRouteHandler } from "@markpdf/nextjs";

export const { POST } = createConvertRouteHandler({
  apiKey: process.env.MARKPDF_API_KEY!,
  defaultOptions: { mode: "fast", clean: true },
  maxUploadBytes: 25 * 1024 * 1024, // optional, rejects with your own 413 before calling the API
});
```

<ParamField body="apiKey" type="string" required>
  Your API key. Always read it from `process.env`, never hardcode it.
</ParamField>

<ParamField body="defaultOptions" type="ConvertOptions" />

<ParamField body="maxUploadBytes" type="number">
  Own limit before forwarding to API — useful for cutting huge uploads at the edge of your app without spending API quota.
</ParamField>

The generated handler:

1. Parse `multipart/form-data` of `Request`.
2. Forward the file to `POST /convert/raw` with `@markpdf/sdk`.
3. Returns the response for API as is (Markdown or JSON), including the correct `content-type`.
4. Translate errors from SDK (`MarkpdfAuthError`, `MarkpdfPayloadTooLargeError`, etc.) to HTTP responses with the same status code.

### Customize the handler

If you need extra logic (auth of your own app, logging, your own rate limiting), use the client directly instead of the all-in-one helper:

```ts app/api/convert/route.ts theme={null}
import { MarkpdfClient } from "@markpdf/sdk";
import { NextRequest, NextResponse } from "next/server";

const client = new MarkpdfClient({ apiKey: process.env.MARKPDF_API_KEY! });

export async function POST(req: NextRequest) {
  const session = await getSession(req);
  if (!session) return NextResponse.json({ error: "no autorizado" }, { status: 401 });

  const form = await req.formData();
  const file = form.get("file") as File;

  try {
    const markdown = await client.convertFile(file, { filename: file.name, mode: "fast" });
    return new NextResponse(markdown, { headers: { "content-type": "text/markdown" } });
  } catch (err) {
    return NextResponse.json({ error: String(err) }, { status: 502 });
  }
}
```

## Server Action: `convertFormData` y `convertUrlAction`

```ts app/actions.ts theme={null}
"use server";

import { convertFormData, convertUrlAction } from "@markpdf/nextjs";

export async function convertUpload(formData: FormData) {
  return convertFormData(formData, {
    apiKey: process.env.MARKPDF_API_KEY!,
    fileField: "file",
    mode: "fast",
  });
}

export async function convertFromSignedUrl(url: string) {
  return convertUrlAction(url, {
    apiKey: process.env.MARKPDF_API_KEY!,
    mode: "fast",
  });
}
```

Server Actions behave like normal Node functions from the client's point of view — Next.js serializes the call for you. You don't need to expose any routes.

<Tip>
  Use Server Actions when the upload form lives in the same component that triggers the conversion (minus boilerplate). Use a Route Handler when another service, a webhook, or a client other than your Next.js app needs to call the endpoint directly.
</Tip>

## Streaming in a Route Handler

```ts app/api/convert/stream/route.ts theme={null}
import { MarkpdfClient } from "@markpdf/sdk";

const client = new MarkpdfClient({ apiKey: process.env.MARKPDF_API_KEY! });

export async function POST(req: Request) {
  const { url } = await req.json();

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for await (const chunk of client.convertStream({ url })) {
        controller.enqueue(encoder.encode(chunk));
      }
      controller.close();
    },
  });

  return new Response(stream, { headers: { "content-type": "text/markdown; charset=utf-8" } });
}
```

Next.js forwards `ReadableStream` back to the client without buffering, so the client starts receiving Markdown before the conversion finishes. See [Streaming and async](/docs/sdks/nextjs/streaming-and-async).

## Runtime: Node vs Edge

`@markpdf/nextjs` works in both Next.js runtimes:

```ts app/api/convert/route.ts theme={null}
export const runtime = "nodejs"; // default; soporta uploads grandes sin lílimits de memoria del edge
// o
export const runtime = "edge"; // colder, but starts faster; stricter payload size limits
```

<Note>
  For large uploads (PDFs of tens of MB) use `runtime = "nodejs"`. The Next.js edge runtime has lower request size limits that depend on your hosting provider.
</Note>
