A production-grade URL shortener built with a layered architecture, distributed ID generation, Redis caching, JWT authentication, and event-driven analytics. Designed to demonstrate real backend engineering practices — not just a "hello world" shortener.
- Visit the live site: https://snap-link-kappa.vercel.app/
- URL Shortening — generate short codes automatically or use a custom alias
- Expiring Links — set an optional expiry date; expired links are cleaned up automatically via MongoDB TTL indexes
- Distributed ID Generation — atomic block-range allocator avoids ID collisions across multiple server instances without a database call on every request
- Redis Caching — cache-first redirect lookups for low-latency responses at scale
- Authentication — JWT-based signup/login so users can manage their own links
- User Dashboard — view, manage, and track all links created by the authenticated user
- QR Code Generation — every short link can generate a scannable QR code
- Event-Driven Analytics — click events (timestamp, referrer, IP, user agent) are published to Kafka/RabbitMQ and processed asynchronously by a background worker, keeping the redirect's critical path fast
- Rate Limiting — per-IP request throttling to prevent abuse and spam link generation
- Input Validation — schema-based request validation using Zod
- Centralized Error Handling — custom typed error classes mapped to consistent HTTP responses
- Testing — unit tests (mocked dependencies) and integration tests (Supertest against a live test database)
- Dockerized — full local dev environment via
docker-compose(API, MongoDB, Redis, Kafka/RabbitMQ)
The project follows a strict layered architecture, where each layer only talks to the layer directly below it:
Routes → Controllers → Services → Repositories → Database
↓
Cache
| Layer | Responsibility | Can call | Cannot call |
|---|---|---|---|
| Routes | Wire URLs to controllers | Controllers | Services, repositories, DB directly |
| Controllers | Parse HTTP requests, shape responses | Services | Repositories, DB, cache directly |
| Services | Core business logic | Repositories, cache, infrastructure | req/res, Express anything |
| Repositories | Data access | DB client only | Cache, services |
| Cache | Fast key-value lookups | Redis client only | DB, services |
This keeps every layer independently testable — services are unit tested with mocked repositories and caches, with zero real database calls required.
| Concern | Technology |
|---|---|
| Runtime | Node.js, Express |
| Database | MongoDB (Mongoose) |
| Cache | Redis |
| Auth | JWT |
| Messaging | Kafka / RabbitMQ |
| Validation | Zod |
| Testing | Jest, Supertest |
| Containerization | Docker, Docker Compose |
| CI/CD | GitHub Actions |
.
├── server.js # Entry point — starts the app, handles graceful shutdown
├── src/
│ ├── app.js # Builds and configures the Express app
│ ├── config/
│ │ ├── index.js # Loads and validates environment variables
│ │ └── database.js # MongoDB connection lifecycle
│ ├── api/
│ │ ├── routes/ # Route definitions
│ │ ├── controllers/ # Request/response handling
│ │ ├── middleware/ # Validation, rate limiting, auth, error handling
│ │ └── validators/ # Zod schemas
│ ├── services/ # Core business logic
│ ├── repositories/ # Data access layer
│ ├── cache/ # Redis cache wrapper
│ ├── models/ # Mongoose schemas
│ ├── infrastructure/
│ │ ├── idGenerator/ # base62 encoding + distributed range allocator
│ │ └── queue/ # Kafka/RabbitMQ producer
│ └── utils/
│ └── errors.js # Custom error classes
├── workers/
│ └── analyticsConsumer.js # Background worker — consumes click events
├── tests/
│ ├── unit/ # Isolated logic tests (mocked dependencies)
│ └── integration/ # Full request/response tests against a real DB
└── docker-compose.yml
- Node.js 18+
- Docker & Docker Compose (recommended for local dev)
- MongoDB and Redis (if not using Docker)
git clone https://github.com/your-username/shortlink-pro.git
cd shortlink-pro
npm installCreate a .env file in the project root:
PORT=3000
NODE_ENV=development
BASE_URL=http://localhost:3000
MONGODB_URI=mongodb://localhost:27017/url_shortener
MONGODB_DB_NAME=url_shortener
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_TTL=3600
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
ID_BLOCK_SIZE=1000
BASE62_ALPHABET=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
KAFKA_BROKER=localhost:9092
KAFKA_CLIENT_ID=url-shortenerdocker-compose up --buildThis starts the API, MongoDB, Redis, and Kafka/RabbitMQ together.
# Start the API server
npm run dev
# In a separate terminal, start the analytics worker
npm run worker| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/signup |
Register a new user |
| POST | /api/v1/auth/login |
Log in, receive a JWT |
| Method | Endpoint | Auth required | Description |
|---|---|---|---|
| POST | /api/v1/shorten |
Optional | Create a short URL |
| GET | /:shortCode |
No | Redirect to the original URL |
| GET | /api/v1/shorten/:shortCode/qr |
No | Get a QR code for a short URL |
Example — create a short URL:
curl -X POST http://localhost:3000/api/v1/shorten \
-H "Content-Type: application/json" \
-d '{
"long_url": "https://example.com/some/very/long/path",
"custom_alias": "my-link",
"expires_at": "2026-12-31T23:59:59Z"
}'Response:
{
"short_url": "http://localhost:3000/my-link"
}| Method | Endpoint | Auth required | Description |
|---|---|---|---|
| GET | /api/v1/dashboard/links |
Yes | List all links created by the authenticated user |
| GET | /api/v1/analytics/:shortCode/stats |
Yes | Click count, timeline, referrer breakdown |
Full interactive API documentation is available via Swagger/OpenAPI at:
http://localhost:3000/api-docs
Instead of hitting the database on every single short-code creation, each server instance atomically reserves a block of 1,000 IDs at once (via a MongoDB atomic $inc on a shared counter document) and hands them out from memory. This means:
- No collisions across concurrent requests or multiple server instances
- ~99.9% of ID generations require zero database calls
- Horizontally scalable without coordination between instances
Redirects check Redis first. On a cache miss, the record is fetched from MongoDB, validated for expiry, and written back into Redis with a TTL — so subsequent requests for the same short code are served without touching the database.
Click events are never awaited on the redirect's critical path. Instead, resolveShortCode() fires an event onto a Kafka/RabbitMQ topic and immediately returns the redirect response. A separate background worker (workers/analyticsConsumer.js) consumes these events and writes aggregated statistics to the database — meaning analytics tracking never adds latency to the user-facing redirect.
# Unit tests (mocked dependencies, no real DB/Redis needed)
npm test
# Integration tests (requires a running test DB — see docker-compose.test.yml)
npm run test:integrationUnit tests cover service-layer logic in isolation (successful creation, duplicate alias handling, expired link handling, cache-hit short-circuiting). Integration tests exercise full request/response cycles against a real database, including validation and rate limiting.
GitHub Actions runs on every push and pull request:
- Install dependencies
- Run lint checks
- Run unit tests
- Run integration tests against a containerized test database
- Build the Docker image
See .github/workflows/ci.yml for the full pipeline.
The app is deployed on [Render / a VPS] with the following setup:
- Dockerized API service
- Managed MongoDB instance
- Managed Redis instance
- Kafka/RabbitMQ broker
- Environment variables configured via the platform's secrets manager
- Redis cache invalidation on link update/delete
- Password-protected links
- Custom domains
- Link preview metadata (Open Graph scraping)
- Structured logging + Prometheus/Grafana monitoring
MIT