Building a Receipt Generation Microservice with Minimal Code
Receipts are one of those deceptively simple artifacts. A checkout completes, an email needs to go out with a PDF attached, and someone downstream (accounting, the customer, a tax authority) needs a stable, printable record. The requirement sounds trivial until you actually build it: HTML-to-PDF rendering, headless browsers, font issues, storage, retries, and versioning all show up quickly.
This post walks through a minimal receipt microservice: one HTTP endpoint, one template, and one dependency for PDF rendering. The goal is a service you can ship in an afternoon and forget about.
The scope
We want a service that:
- Accepts a
POST /receiptsrequest with order data. - Renders a PDF receipt.
- Stores it and returns a URL.
- Optionally emails it.
That's it. No admin UI, no template editor, no user accounts. Keeping scope narrow is how these services stay minimal.
Project setup
We'll use Node.js with Fastify because it's fast to boot and has good schema validation. Any HTTP framework works.
bashmkdir receipts-service && cd receipts-service npm init -y npm install fastify @kamydev/sdk zod
Create server.ts:
tsimport Fastify from "fastify"; import { kamy } from "@kamydev/sdk"; import { z } from "zod"; const app = Fastify({ logger: true }); const ReceiptSchema = z.object({ orderId: z.string(), customer: z.object({ name: z.string(), email: z.string().email(), }), items: z.array(z.object({ description: z.string(), quantity: z.number().int().positive(), unitPrice: z.number(), })), currency: z.string().default("USD"), paidAt: z.string().datetime(), });
The rendering choice
The single biggest decision in a receipt service is how to render the PDF. The usual options:
- Headless Chromium (Puppeteer, Playwright): Full CSS support but heavy. You're managing a browser process, memory limits, and cold starts.
- PDF libraries (pdfkit, pdf-lib): Fast, but you're drawing rectangles and positioning text by hand. Painful for anything that needs to look good.
- Hosted rendering API (Kamy, etc.): You POST data, you get a PDF. No browser to babysit.
For a small service that shouldn't own infrastructure it doesn't need, offloading rendering makes sense. Here's the handler using Kamy's SDK:
tsapp.post("/receipts", async (req, reply) => { const parsed = ReceiptSchema.safeParse(req.body); if (!parsed.success) { return reply.code(400).send({ errors: parsed.error.issues }); } const data = parsed.data; const total = data.items.reduce( (sum, i) => sum + i.quantity * i.unitPrice, 0, ); const pdf = await kamy.documents.render({ template: "receipt", data: { orderId: data.orderId, customer: data.customer, items: data.items, currency: data.currency, paidAt: data.paidAt, total, }, fileName: `receipt-${data.orderId}.pdf`, }); return reply.send({ orderId: data.orderId, url: pdf.url, expiresAt: pdf.expiresAt, }); }); app.listen({ port: 3000 });
The receipt template is defined once in the Kamy dashboard (HTML + CSS, with Handlebars-style placeholders). You don't ship the template in your service repo, which means design changes don't require a redeploy.
The template
Templates live outside your code, but they're still your responsibility. A minimal receipt template looks like this:
html<!DOCTYPE html> <html> <head> <style> body { font-family: system-ui, sans-serif; padding: 40px; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { text-align: left; padding: 8px; border-bottom: 1px solid #eee; } .total { font-weight: bold; text-align: right; margin-top: 20px; } </style> </head> <body> <h1>Receipt #{{orderId}}</h1> <p>Paid by {{customer.name}} on {{paidAt}}</p> <table> <thead> <tr><th>Item</th><th>Qty</th><th>Price</th></tr> </thead> <tbody> {{#each items}} <tr> <td>{{description}}</td> <td>{{quantity}}</td> <td>{{unitPrice}} {{../currency}}</td> </tr> {{/each}} </tbody> </table> <p class="total">Total: {{total}} {{currency}}</p> </body> </html>
Calling the service
From your checkout backend:
httpPOST /receipts HTTP/1.1 Content-Type: application/json { "orderId": "ord_9F2A", "customer": { "name": "Ada Lovelace", "email": "[email protected]" }, "items": [ { "description": "Pro Plan (Annual)", "quantity": 1, "unitPrice": 240 } ], "currency": "USD", "paidAt": "2025-01-15T10:00:00Z" }
Response:
json{ "orderId": "ord_9F2A", "url": "https://cdn.