Every codebase grows a utils/ folder of the same snippets: a retry with backoff, a slugify, a byte formatter, a CSV writer that gets quoting wrong. Lacspace wrote the correct versions of those, once, and shipped them dependency-free — a missing standard library across four small kits.

What’s inside

Utils/Data/DX/Backend packages: id, retry, case, xlsx, csv, humanize, color, money, cache, signed-url, pdf, webhooks
Utils · Data · DX · Backend — the small helpers, done right.

Correct, not copy-pasted

import { retry } from '@lacspace/retry';
const data = await retry(() => fetchFlaky(), { retries: 4, minDelay: 300 });

Real PDFs without spinning up Chromium, money that never loses a cent, CSV that quotes correctly, colour with a built-in WCAG check — the un-glamorous things, done right.

Get started

Browse them all at lacspace.com/packages and read the docs at lacspace.com/docs. They’re part of the 100+ package zero-dependency ecosystem.

Install

Every helper is a separate package, so install only the ones your project needs.

npm i @lacspace/id @lacspace/retry @lacspace/case          # Utils
npm i @lacspace/xlsx @lacspace/csv                         # Data
npm i @lacspace/humanize @lacspace/color                   # DX
npm i @lacspace/signed-url @lacspace/pdf @lacspace/webhooks @lacspace/idempotency  # Backend
npm i @lacspace/cache @lacspace/money @lacspace/markdown   # extras

Quick start by kit

Utils: ids, retries and string case

@lacspace/retry takes retries, minDelay, shouldRetry and onRetry, and uses exponential backoff with full jitter. It also exports withTimeout and a CircuitBreaker.

import { retry, withTimeout, CircuitBreaker } from '@lacspace/retry';
import { uuidv7, prefixedId } from '@lacspace/id';
import { camelCase, kebabCase } from '@lacspace/case';

const data = await retry(() => fetch(url).then((r) => r.json()), {
  retries: 4,
  minDelay: 300,
  shouldRetry: (err) => isTransient(err), // don't retry 4xx
});
await withTimeout((signal) => fetch(url, { signal }), 5000); // TimeoutError after 5s
const breaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 });

uuidv7();                    // time-sortable UUID, index-friendly primary key
prefixedId('user');          // "user_9f8c1a3e..."
kebabCase('XMLHttpRequest'); // "xml-http-request"

Data: Excel and CSV

import { jsonToXlsx, xlsxToJson } from '@lacspace/xlsx';
import { parse, stringify, coerce } from '@lacspace/csv';

const bytes = jsonToXlsx(rows);          // Uint8Array, Dates become real Excel dates
const back = await xlsxToJson(uploaded); // reads Excel and Google Sheets exports

const records = coerce(parse(csvText), { auto: true }); // numbers/booleans inferred
const out = stringify(records, { escapeFormulas: true }); // safe for untrusted data

@lacspace/xlsx builds the ZIP-of-XML file format directly, supports multiple sheets, column widths, per-column numFmt and live formulas, and converts between CSV and XLSX. @lacspace/csv handles quoted fields, escaped quotes, embedded newlines, CRLF and BOMs, and has a CsvStreamParser for chunked input.

DX: readable output and accessible colour

import { bytes, duration, relativeTime, pluralize } from '@lacspace/humanize';
import { contrastRatio, wcagLevel, bestTextColor } from '@lacspace/color';

bytes(1536);                      // "1.5 KB"
duration(90061000);               // "1d 1h"
relativeTime(Date.now() - 3.6e6); // "1 hour ago"
pluralize(3, 'city');             // "3 cities"

wcagLevel(contrastRatio('#767676', '#ffffff')); // "AA"
bestTextColor('#1e90ff');                        // "#000000"

Backend: links, PDFs, webhooks and exactly-once

import { signUrl, verifyUrl } from '@lacspace/signed-url';
import { invoice } from '@lacspace/pdf';
import { verify } from '@lacspace/webhooks';
import { idempotent } from '@lacspace/idempotency';

const link = await signUrl('https://cdn.example.com/files/report.pdf', { secret, expiresIn: 300 });
const ok = await verifyUrl(request.url, { secret }); // { valid, reason? }, never throws

const pdf = invoice({ brand: 'Acme', number: 'INV-1024', date: '2026-08-23',
  items: [{ description: 'Consulting', quantity: 1, rate: 4500 }], currency: '$', taxRate: 13 });

const r = await verify(rawBody, request.headers.get('webhook-signature'), { secret, toleranceSec: 300 });

const { value, replayed } = await idempotent(idempotencyKey, () => chargeCard(order));

Extras: cache, money and Markdown

import { createCache } from '@lacspace/cache';
import { money } from '@lacspace/money';
import { markdownToHtml } from '@lacspace/markdown';

const cache = createCache({ max: 500, ttl: 60_000 });
const user = await cache.wrap(`user:${id}`, () => db.users.find(id)); // concurrent callers share one fetch

money(10, 'USD').allocate([1, 1, 1]).map((m) => m.format()); // ["$3.34", "$3.33", "$3.33"]

markdownToHtml(userMarkdown); // raw HTML in the source is escaped

Why not copy a snippet instead?

The READMEs are candid about where hand-written versions usually go wrong, and these are the cases each package is built to handle:

  • Retries without jitter make every client retry at the same moment; @lacspace/retry uses full jitter and supports AbortSignal.
  • Random UUIDs (v4) scatter inserts across a database index. UUID v7 and ULID sort by creation time, and decodeTime() reads the timestamp back.
  • CSV breaks on commas and newlines inside quoted cells, and cells starting with =, +, - or @ can run as formulas when opened in a spreadsheet. escapeFormulas: true prefixes them with a quote.
  • Money as floats loses cents (0.1 + 0.2 !== 0.3). @lacspace/money stores integer minor units, refuses to add different currencies and knows zero-decimal (JPY) and three-decimal (BHD) currencies.
  • Webhook verification must use the raw body, compare in constant time and reject replays. @lacspace/webhooks does all three and ships presets for Stripe, GitHub and Shopify signatures.
  • PDF generation usually means a dependency-heavy library or a headless browser. @lacspace/pdf writes PDF bytes directly with real Helvetica, Times and Courier metrics, so text wraps and money columns align.

Gotchas worth knowing

  • In-memory stores are per process. @lacspace/idempotency and the webhook dedupe store default to memory; implement their store interfaces over Redis or SQL for multi-instance apps.
  • PNG images are not decoded by @lacspace/pdf. Embed JPEGs with jpegImage(), or decode PNGs elsewhere and pass raw pixels to rgbImage().
  • Money.parse() is best-effort. It can misread ambiguous separators ("1,234" is read as 1.234). For untrusted input, construct from a numeric amount with Money.of or Money.fromMinor.
  • Reading XLSX is async. xlsxToJson and readWorkbook return promises because compressed files need inflating; writing is synchronous.
  • Markdown vs other HTML: markdownToHtml already escapes source HTML. Use sanitizeHtml() only for HTML that came from elsewhere, such as a CMS field.

Each package has its own page on the developer portal, for example retry, xlsx, csv, pdf, webhooks and money, and is published on npm (for example @lacspace/money) under the Lacspace Free Licence v1.0.

Frequently asked questions

What do the Utils, Data, DX and Backend kits cover?

The small, universal helpers every project reaches for — unique IDs, resilient retries and string-case (Utils); real Excel and correct CSV (Data); human-readable formatting and colour maths (DX); and server essentials like signed URLs, PDFs, webhooks and idempotency (Backend). Plus extras like caching, money and markdown. All zero-dependency and isomorphic.

Why not just copy-paste a retry or a slugify?

You can — until the edge cases bite (exponential backoff with jitter, Unicode in slugs, CSV quoting, timezone-correct humanize). These packages are the correct, tested versions of the snippets everyone re-writes, with no dependency tree attached.

Can I really make PDFs without headless Chromium?

Yes — @lacspace/pdf generates real PDFs with zero dependencies and no headless browser (no puppeteer, no pdfkit). It runs on Node, edge and the browser, with batteries-included invoice() and receipt() helpers plus a document builder.

What is idempotency and why do I need it?

It’s how you stop a retried request (a double-clicked “Pay” button, a webhook redelivery) from doing the work twice. @lacspace/idempotency gives you idempotency keys so the same operation runs once, safely.

Is money handling really that tricky?

Floating-point money is a classic bug source. @lacspace/money uses integer minor-units (cents/paisa), safe allocation (splitting a bill without losing a cent) and Intl formatting — so your totals are always exact.