2026
irl
A serverless URL shortener designed for minimal latency: ~350 lines of production code, ~2× as much test code.
- ~350lines of production code
- ~2×lines of tests
- 31tests (unit + E2E)
- 1Lambda, no API Gateway
Context
irl is a serverless URL shortener with a simple thesis: every architecture decision should shorten the path between the click and the redirect. The project was also a lab for a question I care about more than the product itself: how do you test serverless honestly — without a paid account, and without lying to yourself?
The problem
Three problems intersect in a well-built serverless shortener:
- latency: every extra hop (API Gateway, a second service, an unnecessary cold start) shows up in redirect response time;
- short-ID consistency: with a 6-character nanoid, collisions are rare but real — and in serverless there is no shared memory to coordinate;
- testability: DynamoDB and Lambda are hard to run locally, so it’s tempting to test only the happy path and leave E2E “for later” — which is the same as not testing.
How it was built
- A single Lambda function serves the whole app: Nitro bundles SSR and the API routes into one artifact, exposed through a Lambda Function URL — no API Gateway in between.
- On-demand DynamoDB with a single table (
id,url,createdAt). Creation uses aConditionExpressionto guarantee uniqueness without coordination: if the ID already exists, the write fails and the service retries with another one. - Layered server with dependency injection: handler → service → repository. The repository is an interface; swapping the AWS client for the local database is swapping one parameter.
- Infrastructure as code with CDK v2: function, URL, CORS and least-privilege IAM, all defined in the same repository.
- Reproducible environment with Nix, including the Playwright browsers — any machine reproduces the whole setup.
A single Lambda serves SSR and the API behind the Function URL — no API Gateway in the critical path.
Decisions and trade-offs
- Function URL instead of API Gateway. One less component in the critical path and no gateway cost; in exchange, some ready-made capabilities (authentication, WAF, rate limiting) stay out — acceptable for the scope, and documented.
- IDs generated without coordination.
nanoid(6)+ conditional write + bounded retry handles collisions without distributed locking: less infrastructure, more application code — a trade I would make again. - Stateful fakes instead of mocks. Service tests run against a stateful fake repository that behaves like the database; repository tests run against a fake DynamoDB client with condition semantics. A mock that only “expects the call” would not have found the bugs that appeared.
- E2E with a local browser and database. Playwright boots the app against DynamoDB Local — same port, same protocol as the AWS service — instead of simulated responses.
Quality and verification
The project has about twice as much test code as production code (~675 lines against ~350), and the tests cover error paths, not just the happy path: URL validation (including dangerous schemes), ID collision, malformed items, 400/404/500 responses. The process is disciplined: conventional commits, a pre-commit hook running the suite, automated versioning and changelog.
The E2E tests caught two bugs no unit test would have:
- The click before hydration. Playwright clicks as soon as the button is actionable — which on slow connections can happen before Vue hydrates the page. The click becomes a native browser form submit, and the input is lost. The fix is waiting for the framework’s hydration signal before interacting (
waitForFunction), and the app still has an honest pending item: disabling the form until it mounts. - The ghost server. The E2E setup was configured to reuse whatever server was already running on the test port. Result: some runs tested an old build and went green on code that was not the current code. The fix was the strictest one: an occupied port is now an error, never a convenience.
These two cases stand as proof: without a browser and a database, the test lies to you.
Results
The full flow works: shorten with validation, redirect, explicit 404 for unknown IDs — with release 0.2.0 versioned and a changelog generated from commits. What I would finish before calling it complete: server-side redirect (today the redirect resolves on the client, costing extra round-trips — contradicting the project’s own latency thesis), CI running the E2E on every push, and a page with measured latency numbers. The pending items are in the README, in plain sight.
What I learned
Well-designed serverless infrastructure is the kind whose local testing cost is zero: with DynamoDB Local and one bootstrap command, the E2E runs on any machine. And an E2E test that reuses environment state is not E2E — it is an optimistic unit test. Better to fail loudly.