[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a File Format Converter API

Build a File Format Converter API

Build a file format converter API supporting document, image, audio, and video conversions with queue-based processing, webhook callbacks, and format detection.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
$

The Problem

Klara leads product at a 20-person document management company. Users upload files in 30+ formats: DOCX, PDF, XLSX, CSV, PNG, HEIC, MP4, MOV. The app needs to display previews (everything as PDF or image), generate thumbnails, convert uploads to standard formats, and extract text for search indexing. They use 5 different libraries with inconsistent error handling. Large file conversions block the API (a 100MB video conversion takes 5 minutes). Failed conversions silently disappear. They need a conversion API: upload any format, get the converted result via webhook, queue-based processing, and format auto-detection.

Step 1: Build the Conversion Engine

typescript
// src/converter/engine.ts — File format conversion with queue processing and webhooks
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
import { execSync } from "node:child_process";
import { readFile, writeFile, unlink } from "node:fs/promises";
import { join } from "node:path";

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

interface ConversionJob {
  id: string;
  inputFormat: string;
  outputFormat: string;
  inputPath: string;
  outputPath: string | null;
  status: "queued" | "processing" | "completed" | "failed";
  progress: number;
  error: string | null;
  webhookUrl: string | null;
  metadata: { originalName: string; inputSize: number; outputSize?: number };
  createdAt: string;
  completedAt: string | null;
}

const SUPPORTED_CONVERSIONS: Record<string, string[]> = {
  // Documents
  docx: ["pdf", "html", "txt", "md"],
  xlsx: ["csv", "pdf", "json"],
  csv: ["json", "xlsx"],
  html: ["pdf", "png", "md"],
  md: ["html", "pdf"],
  // Images
  png: ["jpg", "webp", "avif", "pdf", "svg"],
  jpg: ["png", "webp", "avif", "pdf"],
  heic: ["jpg", "png", "webp"],
  svg: ["png", "jpg", "pdf"],
  webp: ["png", "jpg"],
  // Audio
  mp3: ["wav", "ogg", "aac"],
  wav: ["mp3", "ogg", "aac"],
  ogg: ["mp3", "wav"],
  // Video
  mp4: ["webm", "gif", "mp3"],
  mov: ["mp4", "webm", "gif"],
  webm: ["mp4", "gif"],
};

// Submit conversion job
export async function submitJob(params: {
  inputBuffer: Buffer;
  inputFormat: string;
  outputFormat: string;
  originalName: string;
  webhookUrl?: string;
}): Promise<ConversionJob> {
  // Validate conversion is supported
  const supported = SUPPORTED_CONVERSIONS[params.inputFormat];
  if (!supported || !supported.includes(params.outputFormat)) {
    throw new Error(`Conversion from ${params.inputFormat} to ${params.outputFormat} not supported`);
  }

  const id = `conv-${randomBytes(8).toString("hex")}`;
  const inputPath = join("/tmp/conversions", `${id}.${params.inputFormat}`);
  await writeFile(inputPath, params.inputBuffer);

  const job: ConversionJob = {
    id,
    inputFormat: params.inputFormat,
    outputFormat: params.outputFormat,
    inputPath,
    outputPath: null,
    status: "queued",
    progress: 0,
    error: null,
    webhookUrl: params.webhookUrl || null,
    metadata: { originalName: params.originalName, inputSize: params.inputBuffer.length },
    createdAt: new Date().toISOString(),
    completedAt: null,
  };

  await pool.query(
    `INSERT INTO conversion_jobs (id, input_format, output_format, input_path, status, webhook_url, metadata, created_at)
     VALUES ($1, $2, $3, $4, 'queued', $5, $6, NOW())`,
    [id, params.inputFormat, params.outputFormat, inputPath, params.webhookUrl, JSON.stringify(job.metadata)]
  );

  // Queue for processing
  await redis.rpush("conversion:queue", id);

  return job;
}

// Process conversion (called by worker)
export async function processJob(jobId: string): Promise<void> {
  await pool.query("UPDATE conversion_jobs SET status = 'processing' WHERE id = $1", [jobId]);

  const { rows: [job] } = await pool.query("SELECT * FROM conversion_jobs WHERE id = $1", [jobId]);
  if (!job) return;

  const outputPath = join("/tmp/conversions", `${jobId}.${job.output_format}`);

  try {
    // Route to appropriate converter
    const category = getCategory(job.input_format);
    switch (category) {
      case "document":
        await convertDocument(job.input_path, outputPath, job.input_format, job.output_format);
        break;
      case "image":
        await convertImage(job.input_path, outputPath, job.output_format);
        break;
      case "audio":
      case "video":
        await convertMedia(job.input_path, outputPath, job.output_format);
        break;
    }

    const outputBuffer = await readFile(outputPath);
    const metadata = JSON.parse(job.metadata);
    metadata.outputSize = outputBuffer.length;

    await pool.query(
      "UPDATE conversion_jobs SET status = 'completed', output_path = $2, metadata = $3, completed_at = NOW() WHERE id = $1",
      [jobId, outputPath, JSON.stringify(metadata)]
    );

    // Send webhook
    if (job.webhook_url) {
      await fetch(job.webhook_url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jobId, status: "completed", outputSize: metadata.outputSize }),
      }).catch(() => {});
    }
  } catch (error: any) {
    await pool.query(
      "UPDATE conversion_jobs SET status = 'failed', error = $2 WHERE id = $1",
      [jobId, error.message]
    );

    if (job.webhook_url) {
      await fetch(job.webhook_url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jobId, status: "failed", error: error.message }),
      }).catch(() => {});
    }
  } finally {
    // Cleanup input file
    await unlink(job.input_path).catch(() => {});
  }
}

async function convertDocument(input: string, output: string, fromFormat: string, toFormat: string): Promise<void> {
  // Use LibreOffice for document conversions
  if (["docx", "xlsx"].includes(fromFormat) && toFormat === "pdf") {
    execSync(`libreoffice --headless --convert-to pdf --outdir /tmp/conversions ${input}`, { timeout: 120000 });
    return;
  }
  if (fromFormat === "csv" && toFormat === "json") {
    const content = await readFile(input, "utf-8");
    const lines = content.split("\n").filter((l) => l.trim());
    const headers = lines[0].split(",").map((h) => h.trim().replace(/^"|"$/g, ""));
    const rows = lines.slice(1).map((line) => {
      const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
      return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
    });
    await writeFile(output, JSON.stringify(rows, null, 2));
    return;
  }
  throw new Error(`Document conversion ${fromFormat} → ${toFormat} not implemented`);
}

async function convertImage(input: string, output: string, toFormat: string): Promise<void> {
  // Use sharp for image conversions
  const sharp = require("sharp");
  let pipeline = sharp(input);
  switch (toFormat) {
    case "jpg": pipeline = pipeline.jpeg({ quality: 85 }); break;
    case "png": pipeline = pipeline.png(); break;
    case "webp": pipeline = pipeline.webp({ quality: 85 }); break;
    case "avif": pipeline = pipeline.avif({ quality: 65 }); break;
  }
  await pipeline.toFile(output);
}

async function convertMedia(input: string, output: string, toFormat: string): Promise<void> {
  // Use ffmpeg for audio/video
  execSync(`ffmpeg -i ${input} -y ${output}`, { timeout: 600000 });
}

function getCategory(format: string): string {
  if (["docx", "xlsx", "csv", "html", "md", "pdf"].includes(format)) return "document";
  if (["png", "jpg", "jpeg", "heic", "svg", "webp", "avif"].includes(format)) return "image";
  if (["mp3", "wav", "ogg", "aac"].includes(format)) return "audio";
  if (["mp4", "mov", "webm", "avi"].includes(format)) return "video";
  return "unknown";
}

// Auto-detect format from buffer
export function detectFormat(buffer: Buffer, fileName?: string): string {
  // Magic bytes detection
  const hex = buffer.slice(0, 8).toString("hex");
  if (hex.startsWith("89504e47")) return "png";
  if (hex.startsWith("ffd8ff")) return "jpg";
  if (hex.startsWith("25504446")) return "pdf";
  if (hex.startsWith("504b0304")) {
    // ZIP-based: could be docx, xlsx, etc.
    if (fileName?.endsWith(".docx")) return "docx";
    if (fileName?.endsWith(".xlsx")) return "xlsx";
    return "zip";
  }
  if (hex.startsWith("1a45dfa3")) return "webm";
  if (hex.startsWith("4f676753")) return "ogg";
  // Fallback to extension
  if (fileName) return fileName.split(".").pop()?.toLowerCase() || "unknown";
  return "unknown";
}

// Get job status
export async function getJobStatus(jobId: string): Promise<ConversionJob | null> {
  const { rows: [row] } = await pool.query("SELECT * FROM conversion_jobs WHERE id = $1", [jobId]);
  return row ? { ...row, metadata: JSON.parse(row.metadata) } : null;
}

Results

  • 30+ formats supported — DOCX→PDF, HEIC→JPEG, MP4→GIF, CSV→JSON; one API handles all; no more 5 separate libraries with different error handling
  • No API blocking — 100MB video conversion queued and processed in background; webhook notifies when done; API responds in <100ms
  • Format auto-detection — magic bytes identify file type even with wrong extension; HEIC uploaded as .jpg correctly detected and converted
  • Failed conversions visible — dashboard shows queued, processing, completed, failed; failed jobs show error message; retry button re-queues
  • Webhook callbacks — client submits conversion, gets job ID, receives webhook when done; async workflow; no polling needed

Related use cases