By Kevin Herschbach | 2026 Jan 16

13 min
Tags
PDF SDK
pdf conversion
next.js
PDF generation is one of the most common requirements in modern web applications. From invoices and reports to certificates and legal documents, businesses need reliable ways to create professional PDFs on demand.
Next.js makes an excellent foundation for building PDF generation services thanks to its API routes and server-side capabilities, but choosing the right PDF SDK can mean the difference between a fragile solution that breaks on edge cases and a production-ready system that scales with your needs.
In this tutorial, you'll learn how to generate PDFs in your Next.js app. To achieve this, you'll define an API route that uses the Apryse Server SDK for PDF generation. You'll then implement image file to PDF conversion as an example of what else is possible.
To build your Next.js PDF generator, you'll follow these steps:
Before you begin, make sure a recent LTS version of Node.js is installed on your machine.
At the time of writing, the current version of the Apryse SDK is v11.8.0, which is compatible with Node.js versions up to v24.x.
To check your currently installed version, run node --version.
In your project directory (we'll go with nextjs-pdf-generation-app in this example), start by initializing npm.
npm init -yThis creates a package.json file.
Next, install Next.js and React ...
npm install next@14.2.35 react@18 react-dom@18... as well as TypeScript and the corresponding type definitions.
npm install --save-dev typescript @types/react @types/react-dom @types/nodeCreate a file named tsconfig.json in your project root with the following content to configure TypeScript behavior for your Next.js project:
Then open package.json and add the necessary Next.js scripts:
Now, install the server-side Apryse SDK (formerly PDFTron, hence the package name), which you'll use to generate PDF files.
npm install @pdftron/pdfnet-nodeSince the Apryse SDK uses native Node.js modules that need special configuration, create a file named next.config.js in your project root with the following content:
This tells Next.js not to bundle the Apryse SDK, since its native modules must run in Node.js.
For your UI, add some CSS by creating a new directory app in your project root and a new file app/globals.css with the following content:
Now you'll create the API that generates PDFs. To use the Apryse SDK for this, you'll need a license key, which you can generate for free in the Apryse SDK documentation.
For easy access to your license key, create a file .env.local in your project root with the following line, replacing your_license_key with your actual key:
APRYSE_LICENSE_KEY=your_license_keyNext, create a new directory app/api/generate-pdf and a new file app/api/generate-pdf/route.ts with the following code:
Let's break this down.
const { PDFNet } = await import('@pdftron/pdfnet-node');await PDFNet.initialize(process.env.APRYSE_LICENSE_KEY);.env.local fileconst doc = await PDFNet.PDFDoc.create();doc.pagePushBack(page);With the API route completed, it's time to build the user interface for your Next.js app.
First, create a new file app/layout.tsx for your root layout.
Then create a new file app/page.tsx for your app's homepage.
Some notes about this page.tsx:
'use client';onClickconst response = await fetch('/api/generate-pdf');const blob = await response.blob();
const url = window.URL.createObjectURL(blob);const link = document.createElement('a');
link.href = url;
link.download = 'empty.pdf';
link.click();window.URL.revokeObjectURL(url);
document.body.removeChild(link);At this point, you have a Next.js app that will generate an empty PDF when the user clicks on the "Generate PDF" button and triggers a download. In the next steps, you'll implement the data upload that makes this actually useful.
For now, just run the app to see if everything is working as intended.
npm run devIf PDF generation fails, check your IDE's console (not the browser's) for issues like a missing license key.
The Apryse SDK supports PDF generation from a wide range of data sources, including from templates and by converting various file formats.
In this example, you'll implement image file to PDF conversion, which requires much more infrastructure for file handling, user feedback, error cases, and state management. Nonetheless, you can use the current state of your app as a basis.
First, you need to change the API route.
Open app/api/generate-pdf/route.ts and replace the code with the following:
Here's an overview of the most important changes to route.ts:
Note that this is still a minimal implementation and not meant for deployment. When building your own production-ready app, consider adding file size limits, more robust MIME type checking, and sanitization of filenames for the output. Also keep in mind what happens if the license key is invalid, how to handle timeouts for large files, and how to manage memory for large uploads.
With the API route updated, you need to adjust the frontend. Open app/page.tsx and replace its content with the following code:
These are the most important changes to page.tsx:
And that's it! You can now use your Next.js app to upload an image and convert it to a PDF.
Run npm run dev to test your creation.
Converting image files is a comparatively straightforward way to generate PDFs. With the Apryse SDK, you can go much further, e.g., with templates that the SDK uses to create fully formatted PDF documents. Other features include PDF manipulation, true redaction, and smart data extraction.
When deploying a Next.js application such as this one, carefully consider your runtime environment. Native bindings only work in a full Node.js environment, not in serverless edge runtimes, and your API route creates temporary files that require filesystem access. Additionally, image to PDF conversion can take several seconds for large files, which may exceed the timeout limits of some serverless platforms.
When it comes to deployment approaches, serverless functions are the easiest option for Next.js applications, but traditional long-running Node.js servers offer better reliability with no cold starts, no timeout restrictions, and are more cost-effective at scale. Container-based deployments provide maximum portability and control and minimize cold start delays compared to traditional serverless functions.
To explore more of what the Apryse SDK has to offer, check out its documentation and the various samples. Start your free trial and contact our sales team to get started.
Tags
PDF SDK
pdf conversion
next.js

Kevin Herschbach
Technical Content Writer
Share this post
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
} "scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverComponentsExternalPackages: ['@pdftron/pdfnet-node']
}
}
module.exports = nextConfig* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
main {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
padding: 20px;
}
.container {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
max-width: 500px;
width: 100%;
}
h1 {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #1f2937;
}
p {
color: #6b7280;
margin-bottom: 1.5rem;
}
button {
width: 100%;
background-color: #2563eb;
color: white;
font-weight: 600;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
button:hover:not(:disabled) {
background-color: #1d4ed8;
}
button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
label {
display: block;
color: #374151;
margin-bottom: 0.5rem;
font-weight: 500;
}
input,
textarea {
width: 100%;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 4px;
margin-bottom: 1rem;
font-size: 1rem;
font-family: inherit;
color: #1f2937;
}
input:focus,
textarea:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
textarea {
resize: vertical;
min-height: 100px;
}
.button-secondary {
background-color: #059669;
margin-top: 0.5rem;
}
.button-secondary:hover:not(:disabled) {
background-color: #047857;
}import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
// Import Apryse SDK dynamically
const { PDFNet } = await import('@pdftron/pdfnet-node');
// Initialize PDFNet
await PDFNet.initialize(process.env.APRYSE_LICENSE_KEY);
// Create a new PDF document
const doc = await PDFNet.PDFDoc.create();
// Create a blank page (US Letter: 8.5" x 11" = 612 x 792 points)
const page = await doc.pageCreate(await PDFNet.Rect.init(0, 0, 612, 792));
// Add the page to the document
doc.pagePushBack(page);
// Save PDF to memory buffer
const buffer = await doc.saveMemoryBuffer(PDFNet.SDFDoc.SaveOptions.e_linearized);
// Convert to Node.js Buffer
const pdfBuffer = Buffer.from(buffer);
// Return the PDF with proper headers
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="empty.pdf"',
},
});
} catch (error) {
console.error('Error generating PDF:', error);
return NextResponse.json(
{
error: 'Failed to generate PDF',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}const page = await doc.pageCreate(await PDFNet.Rect.init(0, 0, 612, 792));const buffer = await doc.saveMemoryBuffer(...);
return new NextResponse(pdfBuffer, { headers: {...} });import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: 'Next.js PDF Generator',
description: 'Generate PDFs using Apryse SDK',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}'use client';
export default function Home() {
const handleGeneratePDF = async () => {
try {
// Call the API route
const response = await fetch('/api/generate-pdf');
if (!response.ok) {
throw new Error('Failed to generate PDF');
}
// Convert response to blob
const blob = await response.blob();
// Create a temporary download link
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'empty.pdf';
// Trigger download
document.body.appendChild(link);
link.click();
// Cleanup
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error:', error);
alert('Failed to generate PDF. Check console for details.');
}
};
return (
<main>
<div>
<button onClick={handleGeneratePDF}>Generate PDF</button>
</div>
</main>
);
}import { NextRequest, NextResponse } from 'next/server';
import { writeFile, unlink } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
export const maxDuration = 60;
export const runtime = 'nodejs';
export async function POST(request: NextRequest) {
let tempInputPath: string | null = null;
let tempOutputPath: string | null = null;
try {
console.log('Image-to-PDF API called');
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
console.log('File received:', file.name, file.type, file.size);
// Validate it's an image
if (!file.type.startsWith('image/')) {
return NextResponse.json(
{ error: 'Invalid file type', details: 'Please upload an image file' },
{ status: 400 }
);
}
// Convert file to buffer
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Create temp files
const timestamp = Date.now();
const fileExtension = file.name.split('.').pop() || 'jpg';
tempInputPath = join(tmpdir(), `input-${timestamp}.${fileExtension}`);
tempOutputPath = join(tmpdir(), `output-${timestamp}.pdf`);
// Write uploaded image to temp file
await writeFile(tempInputPath, buffer);
console.log('Temp file created:', tempInputPath);
// Import Apryse SDK
const { PDFNet } = await import('@pdftron/pdfnet-node');
const licenseKey = process.env.APRYSE_LICENSE_KEY;
if (!licenseKey) {
return NextResponse.json(
{ error: 'License key not configured' },
{ status: 500 }
);
}
await PDFNet.initialize(licenseKey);
console.log('PDFNet initialized');
// Create new PDF document
const doc = await PDFNet.PDFDoc.create();
// Convert image to PDF using Convert class
await PDFNet.Convert.toPdf(doc, tempInputPath);
console.log('Image converted to PDF');
// Save PDF to temp file
await doc.save(tempOutputPath, PDFNet.SDFDoc.SaveOptions.e_linearized);
console.log('PDF saved to temp file');
// Read the PDF file
const fs = await import('fs/promises');
const pdfBuffer = await fs.readFile(tempOutputPath);
console.log('✅ PDF generated:', pdfBuffer.length, 'bytes');
// Clean up temp files
await unlink(tempInputPath);
await unlink(tempOutputPath);
console.log('Temp files cleaned up');
// Return PDF
return new NextResponse(pdfBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${file.name.replace(/\.[^/.]+$/, '')}.pdf"`,
},
});
} catch (error) {
console.error('Error generating PDF:', error);
// Clean up temp files if they exist
try {
if (tempInputPath) await unlink(tempInputPath);
if (tempOutputPath) await unlink(tempOutputPath);
} catch (cleanupError) {
console.error('Error cleaning up temp files:', cleanupError);
}
return NextResponse.json(
{
error: 'Failed to generate PDF',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}"use client";
import { useState } from "react";
export default function Home() {
const [file, setFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [preview, setPreview] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0];
setFile(selectedFile);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result as string);
};
reader.readAsDataURL(selectedFile);
}
};
const handleGeneratePDF = async () => {
if (!file) {
alert("Please select an image first!");
return;
}
setLoading(true);
try {
const formData = new FormData();
formData.append("file", file);
console.log("Uploading file:", file.name);
const response = await fetch("/api/generate-pdf", {
method: "POST",
body: formData,
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.details || "Failed to generate PDF");
}
// Download PDF
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
console.log("✅ PDF downloaded successfully!");
} catch (error) {
console.error("Error:", error);
alert(
`Failed: ${error instanceof Error ? error.message : "Unknown error"}`
);
} finally {
setLoading(false);
}
};
return (
<main>
<div className="container">
<h1>Image to PDF Converter</h1>
<p>Upload an image and convert it to a PDF.</p>
<label htmlFor="file-input">Select Image:</label>
<input
id="file-input"
type="file"
accept="image/*"
onChange={handleFileChange}
/>
{preview && (
<div style={{ marginTop: "1rem", marginBottom: "1rem" }}>
<img
src={preview}
alt="Preview"
style={{
maxWidth: "100%",
maxHeight: "300px",
borderRadius: "4px",
border: "1px solid #d1d5db",
}}
/>
</div>
)}
{file && (
<p
style={{
fontSize: "0.875rem",
color: "#6b7280",
marginBottom: "1rem",
}}
>
Selected: {file.name} ({(file.size / 1024).toFixed(2)} KB)
</p>
)}
<button onClick={handleGeneratePDF} disabled={loading || !file}>
{loading ? "Converting to PDF..." : "Convert to PDF"}
</button>
<p
style={{ fontSize: "0.875rem", color: "#6b7280", marginTop: "1rem" }}
>
💡 Supported: JPG, PNG, GIF, WebP
</p>
</div>
</main>
);
}