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
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.