A microservice built with Express and Puppeteer that generates PDFs from web URLs. This service is designed to be deployed on Railway.
This service provides an API endpoint to convert web pages into high-quality PDF documents. It uses Puppeteer, a headless Chrome browser, to render web pages with full CSS support and then exports them as PDFs.
Key features:
- Creates PDFs from any web URL
- Supports print-specific CSS media queries
- Configurable page size, margins, and scaling
- CORS protection for secure API access
- Node.js 18+
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd ixion-invoice-generator/railway
- Install dependencies:
npm install
- Set up environment variables:
- Create a
.env
file in the root directory (or add to existing one) - Add the required environment variables:
- Create a
PORT=3333
ALLOWED_ORIGINS=http://localhost:3000,https://yourapp.com
To run the service in development mode with hot reloading:
npm run dev
The service will be available at http://localhost:3333 (or the port specified in your .env file).
To run the service in production mode:
- Build the TypeScript files:
npm run build
- Start the service:
npm start
- URL:
/
- Method:
GET
- Response:
{ "status": "ok", "message": "This app is running correctly." }
- URL:
/generate-pdf
- Method:
POST
- Body:
{ "url": "https://example.com/page-to-convert", "printPDF": false }
- Response: PDF file as a downloadable attachment
- Parameters:
url
(required): The URL of the web page to convert to PDFprintPDF
(optional): If true, adds a printPDF parameter to the URL
Here's an example of how to integrate this service in a Nuxt 3 application using a composable function:
// composables/usePdfGenerator.ts
export const usePdfGenerator = () => {
const isGeneratingPdf = ref(false)
const router = useRouter()
// Create toast promise for showing status
let resolveToast: (value: unknown) => void
let rejectToast: (reason?: any) => void
const toastPromise = new Promise((resolve, reject) => {
resolveToast = resolve
rejectToast = reject
})
// Toast notification to show generation status
toast.promise(toastPromise, {
loading: 'Generating PDF...',
success: () => 'PDF generated successfully!',
error: (err: Error) => `Failed to generate PDF: ${err.message}`
})
// Generate PDF function
const generatePdf = async (documentId: string, options = { printPDF: true }) => {
isGeneratingPdf.value = true
try {
// Get the current page URL or a shared URL
const pageUrl = `${useRuntimeConfig().public.appUrl}${router.currentRoute.value.path}`
// Use development or production endpoint
const apiUrl = process.env.NODE_ENV === 'development'
? 'http://localhost:3333/generate-pdf'
: 'https://your-railway-app-url.up.railway.app/generate-pdf'
// Call the PDF generator service
const response = await $fetch(apiUrl, {
method: 'POST',
body: {
url: pageUrl,
printPDF: options.printPDF
},
responseType: 'blob'
}) as Blob
if (!response) {
throw new Error('Failed to generate PDF')
}
// Create and trigger download
const url = window.URL.createObjectURL(response)
const link = document.createElement('a')
link.href = url
link.download = `document-${documentId}.pdf`
link.click()
window.URL.revokeObjectURL(url)
resolveToast(response)
return response
} catch (error) {
console.error('Error generating PDF:', error)
rejectToast(error)
throw error
} finally {
isGeneratingPdf.value = false
}
}
return {
generatePdf,
isGeneratingPdf
}
}
<script setup>
const { generatePdf, isGeneratingPdf } = usePdfGenerator()
const handleDownloadPdf = async () => {
try {
await generatePdf('invoice-123')
} catch (error) {
// Error is already handled by the composable
}
}
</script>
<template>
<div>
<button
@click="handleDownloadPdf"
:disabled="isGeneratingPdf"
>
{{ isGeneratingPdf ? 'Generating PDF...' : 'Download PDF' }}
</button>
</div>
</template>
This service is configured for easy deployment on Railway.
- Connect your repository to Railway
- Set the required environment variables in the Railway dashboard
- Deploy your application
The service implements CORS protection that restricts access to specified origins only. Make sure to properly configure the ALLOWED_ORIGINS
environment variable in production.