Terminal.skills
Use Cases/Build a QR Code Generator API

Build a QR Code Generator API

Build a QR code generation API with custom styling, logo embedding, dynamic QR codes with editable destinations, scan analytics, and batch generation — powering marketing campaigns and product packaging.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor
$

The Problem

Liam leads marketing at a 20-person consumer brand. They print QR codes on product packaging, event materials, and ads. Each QR code is generated manually in a free online tool, downloaded, and sent to the printer. When a campaign URL changes, they can't update printed QR codes — they've wasted $15K on packaging with dead links. They have no idea which QR codes get scanned, from where, or on what devices. They need dynamic QR codes (URL can change after printing), branded with their logo, and tracked with scan analytics.

Step 1: Build the QR Code Engine

typescript
// src/qr/generator.ts — QR code generation with dynamic links and scan tracking
import QRCode from "qrcode";
import sharp from "sharp";
import { pool } from "../db";
import { Redis } from "ioredis";
import { createHash } from "node:crypto";

const redis = new Redis(process.env.REDIS_URL!);

interface QRCodeConfig {
  id: string;
  name: string;
  type: "static" | "dynamic";
  destinationUrl: string;
  shortCode: string;           // for dynamic QR: resolves to destination
  style: {
    foreground: string;        // hex color
    background: string;
    cornerRadius: number;      // 0-50
    logoUrl: string | null;
    logoSize: number;          // percentage of QR size (10-30)
    errorCorrection: "L" | "M" | "Q" | "H";
    size: number;              // pixels
    margin: number;
  };
  scanCount: number;
  createdBy: string;
  createdAt: string;
}

// Create a QR code
export async function createQRCode(
  name: string,
  destinationUrl: string,
  options?: {
    type?: "static" | "dynamic";
    foreground?: string;
    background?: string;
    logoUrl?: string;
    logoSize?: number;
    size?: number;
    userId?: string;
  }
): Promise<{ qrCode: QRCodeConfig; imageBuffer: Buffer; imageUrl: string }> {
  const id = `qr-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
  const type = options?.type || "dynamic";
  const shortCode = generateShortCode();

  // For dynamic QR, the encoded URL is our redirect endpoint
  const encodedUrl = type === "dynamic"
    ? `${process.env.APP_URL}/q/${shortCode}`
    : destinationUrl;

  const style: QRCodeConfig["style"] = {
    foreground: options?.foreground || "#000000",
    background: options?.background || "#FFFFFF",
    cornerRadius: 0,
    logoUrl: options?.logoUrl || null,
    logoSize: options?.logoSize || 20,
    errorCorrection: options?.logoUrl ? "H" : "M", // H for logo overlay
    size: options?.size || 1024,
    margin: 2,
  };

  // Generate QR code
  const imageBuffer = await generateQRImage(encodedUrl, style);

  // Store config
  const qrCode: QRCodeConfig = {
    id, name, type, destinationUrl, shortCode,
    style, scanCount: 0,
    createdBy: options?.userId || "",
    createdAt: new Date().toISOString(),
  };

  await pool.query(
    `INSERT INTO qr_codes (id, name, type, destination_url, short_code, style, created_by, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
    [id, name, type, destinationUrl, shortCode, JSON.stringify(style), options?.userId]
  );

  // Cache redirect for dynamic QR
  if (type === "dynamic") {
    await redis.set(`qr:redirect:${shortCode}`, JSON.stringify({ id, url: destinationUrl }));
  }

  // Cache generated image
  const imageKey = `qr:image:${id}`;
  await redis.setex(imageKey, 86400 * 30, imageBuffer);

  return {
    qrCode,
    imageBuffer,
    imageUrl: `${process.env.APP_URL}/api/qr/${id}/image`,
  };
}

// Generate QR image with custom styling
async function generateQRImage(url: string, style: QRCodeConfig["style"]): Promise<Buffer> {
  // Generate base QR code
  const qrBuffer = await QRCode.toBuffer(url, {
    errorCorrectionLevel: style.errorCorrection,
    width: style.size,
    margin: style.margin,
    color: {
      dark: style.foreground,
      light: style.background,
    },
  });

  let image = sharp(qrBuffer);

  // Overlay logo in center
  if (style.logoUrl) {
    const logoResponse = await fetch(style.logoUrl);
    const logoBuffer = Buffer.from(await logoResponse.arrayBuffer());

    const logoPixels = Math.round(style.size * (style.logoSize / 100));
    const resizedLogo = await sharp(logoBuffer)
      .resize(logoPixels, logoPixels, { fit: "contain", background: { r: 255, g: 255, b: 255, alpha: 1 } })
      .toBuffer();

    // Add white padding around logo
    const padding = 8;
    const paddedLogo = await sharp({
      create: {
        width: logoPixels + padding * 2,
        height: logoPixels + padding * 2,
        channels: 4,
        background: style.background,
      },
    }).composite([{
      input: resizedLogo,
      left: padding,
      top: padding,
    }]).png().toBuffer();

    const offset = Math.round((style.size - logoPixels - padding * 2) / 2);
    image = image.composite([{
      input: paddedLogo,
      left: offset,
      top: offset,
    }]);
  }

  return image.png().toBuffer();
}

// Resolve dynamic QR code (redirect endpoint)
export async function resolveQR(shortCode: string, scanData: {
  ip: string; userAgent: string; referrer: string;
}): Promise<string | null> {
  const cached = await redis.get(`qr:redirect:${shortCode}`);
  if (!cached) return null;

  const { id, url } = JSON.parse(cached);

  // Track scan (non-blocking)
  trackScan(id, shortCode, scanData).catch(() => {});

  return url;
}

// Update destination URL (dynamic QR only)
export async function updateDestination(qrId: string, newUrl: string): Promise<void> {
  const { rows: [qr] } = await pool.query(
    "SELECT short_code, type FROM qr_codes WHERE id = $1", [qrId]
  );
  if (!qr || qr.type !== "dynamic") throw new Error("Can only update dynamic QR codes");

  await pool.query("UPDATE qr_codes SET destination_url = $2 WHERE id = $1", [qrId, newUrl]);
  await redis.set(`qr:redirect:${qr.short_code}`, JSON.stringify({ id: qrId, url: newUrl }));
}

// Track scan analytics
async function trackScan(qrId: string, shortCode: string, data: {
  ip: string; userAgent: string; referrer: string;
}): Promise<void> {
  const day = new Date().toISOString().slice(0, 10);

  await redis.hincrby(`qr:scans:${qrId}:daily`, day, 1);
  await redis.incr(`qr:scans:total:${qrId}`);

  // Parse device
  const device = data.userAgent.includes("Mobile") ? "mobile" : "desktop";
  await redis.hincrby(`qr:scans:${qrId}:device`, device, 1);

  await pool.query(
    `INSERT INTO qr_scans (qr_id, ip_hash, device, user_agent, referrer, scanned_at)
     VALUES ($1, $2, $3, $4, $5, NOW())`,
    [qrId, createHash("md5").update(data.ip).digest("hex").slice(0, 12),
     device, data.userAgent.slice(0, 200), data.referrer]
  );
}

// Get scan analytics
export async function getScanAnalytics(qrId: string): Promise<{
  totalScans: number;
  scansByDay: Record<string, number>;
  scansByDevice: Record<string, number>;
  uniqueScans: number;
}> {
  const [total, daily, devices, unique] = await Promise.all([
    redis.get(`qr:scans:total:${qrId}`),
    redis.hgetall(`qr:scans:${qrId}:daily`),
    redis.hgetall(`qr:scans:${qrId}:device`),
    pool.query("SELECT COUNT(DISTINCT ip_hash) as unique_count FROM qr_scans WHERE qr_id = $1", [qrId]),
  ]);

  return {
    totalScans: parseInt(total || "0"),
    scansByDay: Object.fromEntries(Object.entries(daily).map(([k, v]) => [k, parseInt(v)])),
    scansByDevice: Object.fromEntries(Object.entries(devices).map(([k, v]) => [k, parseInt(v)])),
    uniqueScans: parseInt(unique.rows[0].unique_count),
  };
}

// Batch generate QR codes (for product packaging)
export async function batchGenerate(items: Array<{
  name: string; url: string; sku?: string;
}>, style?: Partial<QRCodeConfig["style"]>): Promise<Array<{ name: string; imageBuffer: Buffer }>> {
  const results = [];
  for (const item of items) {
    const { imageBuffer } = await createQRCode(item.name, item.url, {
      type: "dynamic",
      ...style,
    });
    results.push({ name: item.name, imageBuffer });
  }
  return results;
}

function generateShortCode(): string {
  const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
  return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
}

Results

  • $15K packaging waste eliminated — dynamic QR codes let them change the destination URL after printing; expired campaign → updated to new landing page in 5 seconds
  • Scan analytics reveal campaign performance — "The subway poster got 3,200 scans (82% mobile), the flyer got 45" — budget reallocated to high-performing channels
  • Branded QR codes with logo — company logo embedded in center; QR codes look professional on packaging instead of generic black-and-white squares
  • Batch generation for product lines — 500 SKU-specific QR codes generated in 2 minutes; each links to the product page and tracks scans independently
  • Unique vs total scans — IP hashing distinguishes "100 people scanned" from "1 person scanned 100 times"; accurate reach measurement