pdf-raster

Next.js upload and preview example

Build a simple App Router flow with a client upload form, a Node.js route handler, and image previews for every returned page.

Keep the native package on the server

Import @omsimos/pdf-raster only inside the route handler. The client page should only send FormData and render the returned image payloads.

This example shows a complete App Router flow:

The client page uploads a PDF with FormData.

The route handler validates the upload, converts the PDF, and returns one response object per page.

The client renders every returned image with next/image.

File structure

page.tsx
route.ts

Route handler

import { NextResponse } from "next/server";
import { convert, PdfToImagesError } from "@omsimos/pdf-raster";

export const runtime = "nodejs";

function errorResponse(code: string, message: string, status: number) {
  return NextResponse.json({ code, message }, { status });
}

export async function POST(request: Request) {
  try {
    const formData = await request.formData();
    const upload = formData.get("file");

    if (!(upload instanceof File)) {
      return errorResponse("INVALID_INPUT", "Missing PDF upload.", 400);
    }

    const lowerName = upload.name.toLowerCase();
    const isPdf =
      upload.type === "application/pdf" || lowerName.endsWith(".pdf");

    if (!isPdf) {
      return errorResponse("INVALID_INPUT", "Only PDF uploads are supported.", 400);
    }

    const buffer = Buffer.from(await upload.arrayBuffer());
    const pages = await convert(buffer, {
      dpi: 300,
      outputFormat: "png",
    });

    return NextResponse.json({
      pages: pages.map((page) => ({
        pageIndex: page.pageIndex,
        width: page.width,
        height: page.height,
        dpi: page.dpi,
        mimeType: page.mimeType,
        src: `data:${page.mimeType};base64,${page.data.toString("base64")}`,
      })),
    });
  } catch (error) {
    if (error instanceof PdfToImagesError) {
      return errorResponse(error.code, error.message, 400);
    }

    return errorResponse(
      "RENDER_ERROR",
      "Unexpected conversion failure.",
      500,
    );
  }
}

Client page

"use client";

import Image from "next/image";
import { useState } from "react";

type ConvertedPage = {
  pageIndex: number;
  width: number;
  height: number;
  dpi: number;
  mimeType: string;
  src: string;
};

export default function Page() {
  const [file, setFile] = useState<File | null>(null);
  const [pages, setPages] = useState<ConvertedPage[]>([]);
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();

    if (!file) {
      setError("Choose a PDF before converting.");
      return;
    }

    setError(null);
    setIsLoading(true);
    setPages([]);

    const formData = new FormData();
    formData.set("file", file);

    const response = await fetch("/api/convert", {
      method: "POST",
      body: formData,
    });

    const payload = await response.json();

    if (!response.ok) {
      setError(payload.message ?? "Conversion failed.");
      setIsLoading(false);
      return;
    }

    setPages(payload.pages ?? []);
    setIsLoading(false);
  }

  return (
    <main>
      <form onSubmit={handleSubmit}>
        <input
          type="file"
          accept="application/pdf,.pdf"
          onChange={(event) => setFile(event.currentTarget.files?.[0] ?? null)}
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? "Converting..." : "Convert PDF"}
        </button>
      </form>

      {error ? <p>{error}</p> : null}

      <section>
        {pages.map((page) => (
          <article key={page.pageIndex}>
            <h2>Page {page.pageIndex + 1}</h2>
            <p>
              {page.width} × {page.height} at {page.dpi} DPI
            </p>
            <Image
              src={page.src}
              alt={`Rendered PDF page ${page.pageIndex + 1}`}
              width={page.width}
              height={page.height}
              unoptimized
            />
          </article>
        ))}
      </section>
    </main>
  );
}

Why this pattern works

  • the browser never imports the native package
  • the route stays small and can normalize errors for the UI
  • the client gets everything it needs to render all page previews immediately

Good for docs, demos, and small previews

Returning data: URLs keeps the example easy to understand. For larger jobs, prefer storing the rendered images server-side and returning references instead of base64-encoding every page into JSON.

If you only need the API endpoint and not the upload UI, use the smaller route-handler example instead.

On this page