A high-performance URL-shortening REST API built with Fastify, TypeScript, and PostgreSQL. The project follows Clean Architecture principles and emphasizes observability, security, and scalability.
Interactive API documentation in Swagger.
Why I Built This Project
I enjoy building REST APIs and wanted to understand how services such as Bitly and TinyURL work behind the scenes. The project was designed to demonstrate:
- Clean Architecture – Clear separation of responsibilities.
- Observability – Metrics, structured logs, and distributed traces.
- Performance – In-memory caching and optimized indexes.
- Security – Input validation and rate limiting.
- Containerization – A complete Docker Compose infrastructure.
I selected Fastify because of its low overhead, native TypeScript support, and strong performance. PostgreSQL and Drizzle ORM provide reliable relational persistence without the weight of a larger ORM.
Project Architecture
url-shortcut/
├── src/
│ ├── core/ # Shared entities, types, and Either
│ ├── domain/urls/
│ │ ├── application/ # Repository contracts and use cases
│ │ └── enterprise/entities/ # Domain models
│ ├── infra/
│ │ ├── cache/ # In-memory cache
│ │ ├── database/ # Drizzle repositories and schemas
│ │ ├── env/ # Environment validation
│ │ ├── http/ # Controllers, presenters, and routes
│ │ ├── logging/ # Structured logging
│ │ ├── metrics/ # Prometheus metrics
│ │ └── utils/ # Infrastructure utilities
│ └── types/ # Global TypeScript definitions
├── config/ # Grafana, Loki, Tempo, Traefik, and OTel
└── test/ # Unit and end-to-end tests
Architectural Decisions
| Area | Decision | Reason |
|---|---|---|
| Framework | Fastify 5 | Low overhead and excellent performance |
| Database | PostgreSQL + Drizzle | ACID guarantees, JSON support, and maturity |
| Cache | In-memory Map | Five-minute TTL for frequently visited URLs |
| Logs | Structured JSON | Direct Loki and Grafana integration |
| Proxy | Traefik 3 | Service discovery and Let’s Encrypt support |
| Observability | OpenTelemetry | Vendor-neutral industry standard |
Technologies and Tools
- Node.js 22, TypeScript 5, Fastify 5, and PostgreSQL 15+.
- Drizzle ORM for lightweight, typed persistence.
- Docker and Traefik 3 for deployment and reverse proxying.
- Prometheus, Grafana, Loki, Tempo, and OpenTelemetry for observability.
- Pino for structured JSON logs.
- Zod for runtime schema validation and end-to-end typing.
Features
Core Features
- Automatic short-code generation.
- Redirects with click, IP, user-agent, and geolocation tracking.
- Click analytics for each link.
- Optional expiration dates.
- Configurable rate limits.
Performance
- Five-minute in-memory cache for frequently accessed URLs.
- Optimized indexes on
short_codeandcreated_at. - Non-blocking analytics tracking.
Security
- Blocks dangerous protocols such as
javascript:anddata:. - Prevents redirects to private network addresses.
- End-to-end validation with Zod and TypeScript.
Observability
- Prometheus metrics for latency, cache hits, and clicks.
- Correlated structured request logs.
- Liveness, readiness, and complete health checks.
- Distributed tracing with OpenTelemetry.
Technical Challenges
1. Redirect Performance
Redirects must be extremely fast. A five-minute in-memory TTL cache avoids unnecessary database queries for popular short codes.
const cachedUrl = cache.get(shortCode);
if (cachedUrl) {
metrics.cacheHits.inc();
return redirect(cachedUrl);
}
const url = await urlRepository.findByShortCode(shortCode);
if (url) cache.set(shortCode, url.originalUrl, TTL);
metrics.cacheMisses.inc();
2. Asynchronous Analytics
Click tracking must not increase redirect latency. The response is returned immediately while analytics is persisted asynchronously in the background.
redirectAndTrackUrlUseCase.execute(shortCode, request)
.then((url) => reply.redirect(url.originalUrl))
.catch(() => {
// An analytics failure must not break the redirect.
});
3. Complete Observability
The production stack combines custom Prometheus metrics, request-correlated JSON logs, OpenTelemetry traces, and ready-to-use Grafana dashboards.
const httpDuration = new Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests",
labelNames: ["method", "route", "status_code"],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
});
4. URL Validation
Zod validation blocks dangerous protocols and private IP addresses before a link is stored.
const urlSchema = z.string()
.url()
.refine((url) => !["javascript:", "data:", "vbscript:"].includes(url.protocol), {
message: "Invalid protocol",
})
.refine((url) => !isPrivateIP(url), {
message: "Private IPs not allowed",
});
What I Learned
Technical Skills
- Fastify plugins, hooks, and schema validation.
- Drizzle queries, migrations, and schema design.
- Clean Architecture and dependency injection.
- Docker Compose and a complete observability stack.
- Rate-limiting and caching strategies.
Engineering Skills
- REST design and correct status-code selection.
- Swagger and OpenAPI documentation.
- Database indexing and latency optimization.
- Debugging through structured logs and traces.
API Endpoints
# Create a short URL
POST /api/links
{ "url": "https://example.com/very/long/url" }
# Redirect
GET /{shortCode}
# Manage links and analytics
GET /api/links
DELETE /api/links/{shortCode}
GET /api/analytics/{shortCode}
# Health and metrics
GET /health
GET /health/live
GET /health/ready
GET /metrics
Available Scripts
# Development and build
npm run dev
npm run build
npm run start
# Tests
npm test
npm run test:unit
npm run test:e2e
npm run test:watch
# Database
npx drizzle-kit push
npx drizzle-kit generate
# Infrastructure
docker-compose up -d
docker-compose logs -f
Next Steps
- JWT authentication for link ownership.
- Web dashboard for managing URLs.
- QR-code generation.
- Custom short codes.
- Visual analytics dashboard.
- Redis-backed distributed caching.
Why This Project Matters
URL Shortcut demonstrates high-performance API design, clean and testable boundaries, end-to-end observability, Docker-based infrastructure, and security practices such as strict validation and rate limiting. It is a practical exploration of building robust, observable Node.js services rather than only a basic URL shortener.