A production-ready URL Shortener built with Node.js, Express, MongoDB, Redis, and Docker, following a layered architecture with Repository-Service pattern, Redis caching, rate limiting, validation, structured logging, and complete API documentation.
- Project Overview
- Features
- Tech Stack
- High Level Architecture
- System Design
- Request Lifecycle
- Data Flow
- Redis Cache Flow
- Project Philosophy
This project is designed to simulate how modern backend systems are developed in production environments.
Unlike traditional CRUD applications, the architecture separates concerns into independent layers including Controllers, Services, Repositories, Models, Validators, Middleware, Utilities, and Configuration modules.
The objective is not only to shorten URLs but also to demonstrate scalable backend engineering practices such as:
- Clean Architecture
- Separation of Concerns
- Repository Pattern
- Service Layer
- Redis Caching
- Rate Limiting
- Centralized Error Handling
- Structured Logging
- Dockerized Deployment
- API Documentation
Every request travels through multiple well-defined layers before reaching the database, making the application easier to extend, debug, maintain, and test.
- Create Short URLs
- Redirect using Short Code
- Retrieve URL Information
- Expiration Support
- Duplicate URL Handling
- Redis Cache
- MongoDB Persistence
- Fast URL Lookup
- Optimized Database Queries
- Lightweight Responses
- Global Error Handler
- Request Validation
- Structured Logging
- Production Configuration
- Graceful Error Responses
- Request Rate Limiting
- Advanced Rate Limiting
- Input Validation
- URL Sanitization
- HTTP Security Best Practices
- Swagger Documentation
- Docker Compose
- Environment Configuration
- ESLint
- Prettier
- Unit Testing
- Performance Testing
| Layer | Technology |
|---|---|
| Runtime | Node.js |
| Framework | Express.js |
| Database | MongoDB |
| ODM | Mongoose |
| Cache | Redis |
| Validation | Joi |
| Logging | Winston |
| Documentation | Swagger |
| Containers | Docker |
| Orchestration | Docker Compose |
| Testing | Jest |
| Linting | ESLint |
| Formatting | Prettier |
Client
β
HTTP Request
β
Express Router
β
Validation Layer
β
Rate Limiting Layer
β
Controller Layer
β
Service Layer
β
Repository Pattern
β β
β β
Redis Cache MongoDB
β β
ββββββββ¬βββββββββ
β
Business Result
β
Response Formatter
β
HTTP Response
flowchart LR
A[Client / Browser]
B[Express Application]
C[Rate Limiter]
D[Controllers]
E[Service Layer]
F[Repository Layer]
G[(Redis)]
H[(MongoDB)]
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
F --> H
The current deployment is ideal for:
- Local development
- Learning backend architecture
- Small production deployments
- Personal projects
- MVP applications
Every request follows the following path:
Client
β
Express Server
β
Validation Middleware
β
Rate Limiter
β
Controller
β
Service
β
Repository
β
Redis Cache
β
MongoDB
β
JSON Response
The architecture intentionally separates business logic from infrastructure logic, making it easy to introduce additional services in the future.
Although the current implementation runs as a single application, the codebase is intentionally structured so it can evolve into a distributed system.
flowchart LR
Client
β
DNS
β
Load Balancer
β
Node 1
β
Redis
β
MongoDB
Load Balancer --> Node2
Load Balancer --> Node3
Node2 --> Redis
Node3 --> Redis
Redis --> MongoDB
As traffic increases, additional infrastructure components can be introduced without significant changes to the application's internal code.
flowchart LR
Client
β
DNS
β
CDN
β
Nginx
β
Load Balancer
β
API Server 1
β
Redis Cluster
β
MongoDB Replica Set
Load Balancer --> API Server 2
Load Balancer --> API Server 3
API Server 2 --> Redis Cluster
API Server 3 --> Redis Cluster
Redis Cluster --> MongoDB Replica Set
API Server 1 --> Logging
API Server 2 --> Logging
API Server 3 --> Logging
Logging --> Grafana
Logging --> Prometheus
| Component | Responsibility |
|---|---|
| Client | Sends HTTP requests to the API |
| Load Balancer | Distributes traffic across multiple application instances |
| Express Application | Handles incoming requests |
| Validation Middleware | Validates incoming payloads |
| Rate Limiter | Prevents abuse and request flooding |
| Controller | Handles HTTP-specific logic |
| Service Layer | Executes business rules |
| Repository Layer | Performs database operations |
| Redis | High-speed caching layer |
| MongoDB | Persistent storage |
| Winston | Structured logging |
| Docker | Containerization |
| Docker Compose | Local orchestration |
The project is designed so each layer can scale independently.
- Multiple Node.js instances can be added behind a load balancer.
- Redis can evolve into a Redis Cluster.
- MongoDB can be upgraded to a Replica Set or Sharded Cluster.
- Background workers can be introduced for analytics and cleanup jobs.
- Reverse proxies such as Nginx can provide SSL termination, compression, and request routing.
- Monitoring systems such as Prometheus and Grafana can be integrated for observability.
Because the business logic is isolated from infrastructure concerns, these upgrades require minimal modifications to the existing codebase.
Instead of placing everything inside route handlers, responsibilities are divided.
For example:
Routes
Only map endpoints.
Example:
POST /api/url
Routes never contain business logic.
Controllers
Controllers understand HTTP.
They receive requests and send responses.
Responsibilities:
- Read request body
- Read params
- Call Services
- Return Response
Controllers never communicate with MongoDB directly.
Services
Services contain the actual business logic.
Examples:
- Generate Short Code
- Check Cache
- Handle Duplicate URLs
- Manage Expiration
- Apply Business Rules
Services know nothing about Express.
Repositories
Repositories are the only layer allowed to communicate with MongoDB.
Benefits:
- Database logic stays isolated.
- Easy migration to another database.
- Easier testing using mocked repositories.
Suppose the client sends
POST /api/url
The request travels like this:
Client
β
Express
β
Routes
β
Validation Middleware
β
Rate Limiter
β
Controller
β
Service
β
Repository
β
MongoDB
β
Repository
β
Service
β
Controller
β
Response Utility
β
Client
Every layer has exactly one responsibility.
Client sends
POST /api/url
β
Express receives the request.
β
The request is matched with
routes/url.routes.js
β
Validation middleware validates the payload using Joi.
Invalid payloads never reach controllers.
β
Rate limiter checks whether the client exceeded limits.
β
Controller receives a validated request.
β
Controller delegates work to the Service Layer.
β
Service checks Redis.
If URL exists:
Return cached response
Else
β
Generate short code
β
Call Repository
β
Save MongoDB
β
Store Redis Cache
β
Return response
β
Controller
β
JSON Response
Browser
β
Express
β
Router
β
Middleware
β
Controller
β
Service
β
Repository
β
MongoDB
β
Repository
β
Service
β
Redis Cache
β
Controller
β
Client
Notice that Controllers never know how MongoDB works.
Similarly, MongoDB never knows anything about HTTP requests.
This separation keeps the architecture clean.
Redis significantly reduces database load.
Incoming Request
β
Check Redis
β
Cache Hit?
YES
β
βΌ
Return Cached Data
β
βΌ
Complete Request
If cache misses:
Redis Miss
β
MongoDB Query
β
Store Result in Redis
β
Return Response
This approach reduces response latency and minimizes unnecessary database reads.
Every request is logged using Winston.
Incoming Request
β
Logger
β
Business Layer
β
Database
β
Logger
β
Response
β
Log File
Logs help in:
- Debugging
- Monitoring
- Production Troubleshooting
- Performance Analysis
Instead of manually handling errors everywhere:
throw Error()
β
Global Error Handler
β
Standard Response
β
Client
Every error follows the same response format, improving consistency across the API.
This repository is intentionally designed to resemble backend services used in production environments rather than a basic CRUD application.
The emphasis is on clean code organization, maintainability, scalability, and separation of concerns. By isolating routing, validation, business logic, persistence, caching, and error handling into dedicated layers, the project becomes significantly easier to extend and maintain as new features are introduced.
Whether you're exploring backend architecture, preparing for technical interviews, or looking for a reference implementation of a modern Node.js service, this project aims to provide a practical example of production-oriented backend engineering.
.
βββ docker-compose.yml
βββ Dockerfile
βββ .dockerignore
βββ .env
βββ .env.example
βββ eslint.config.js
βββ .eslintrc.json
βββ .gitignore
βββ LICENSE
βββ MakeFile
βββ mongo-init.js
βββ package.json
βββ package-lock.json
βββ README.md
βββ src
β βββ app.js
β βββ server.js
β βββ config/
β βββ constants/
β βββ controllers/
β βββ docs/
β βββ middlewares/
β βββ models/
β βββ public/
β βββ repositories/
β βββ routes/
β βββ scripts/
β βββ services/
β βββ tests/
β βββ utils/
β βββ validators/
The repository follows a modular architecture where every folder owns a single responsibility. This design minimizes coupling, improves maintainability, and allows features to evolve independently without affecting unrelated parts of the application.
The application entry point is intentionally lightweight. As the request progresses through the application, each layer performs a dedicated task before passing control to the next layer.
The overall execution flow can be summarized as:
server.js
β
βΌ
app.js
β
βΌ
Routes
β
βΌ
Middlewares
β
βΌ
Controllers
β
βΌ
Services
β
βΌ
Repositories
β
βΌ
MongoDB / Redis
The root directory contains the configuration files required for development, production deployment, containerization, linting, formatting, testing, and dependency management.
| File | Purpose |
|---|---|
| Dockerfile | Builds the application container |
| docker-compose.yml | Starts the complete application stack |
| .env | Local environment variables |
| .env.example | Template for environment configuration |
| package.json | Project metadata and dependencies |
| package-lock.json | Dependency lock file |
| MakeFile | Common development commands |
| mongo-init.js | MongoDB initialization script |
| LICENSE | Open source license |
| README.md | Project documentation |
| eslint.config.js | ESLint configuration |
| .prettierrc | Prettier formatting rules |
The src directory contains the complete application source code.
Every production component lives inside this directory.
src/
The application avoids placing business logic in the root directory, resulting in a cleaner project structure.
This file acts as the application bootstrap.
Responsibilities include:
- Starting the HTTP server
- Listening on the configured port
- Handling startup events
- Logging server initialization
- Graceful shutdown
This file should remain minimal and should never contain business logic.
The Express application is configured here.
Typical responsibilities include:
- Creating the Express instance
- Registering middleware
- Loading routes
- Configuring error handling
- Initializing Swagger
- Loading public assets
Think of app.js as the composition root of the application.
config/
This directory centralizes every external dependency.
Current configuration modules include:
Responsible for:
- MongoDB connection
- Connection retry
- Connection events
- Graceful shutdown
Creates and exports the Redis client.
Responsibilities:
- Redis connection
- Cache initialization
- Reconnection
- Error handling
Provides a centralized interface for reading environment variables.
Instead of directly using:
process.env.PORT
the application imports values from this module, improving consistency and simplifying configuration management.
constants/
Stores reusable constants used throughout the application.
Examples include:
- HTTP status codes
- Cache expiration values
- Default limits
- Route prefixes
- Application constants
Centralizing constants prevents duplicated values throughout the codebase.
routes/
Routes define the public API of the application.
Responsibilities:
- Mapping endpoints
- Registering middleware
- Connecting controllers
Routes never contain business logic.
Example:
POST /api/url
β
Controller
Routes remain intentionally thin and act only as the application's navigation layer.
Controllers understand HTTP.
Their responsibility is translating incoming HTTP requests into service calls.
Typical responsibilities include:
- Reading request body
- Reading route parameters
- Reading query parameters
- Calling service methods
- Returning formatted responses
Controllers should never:
- Access MongoDB
- Generate hashes
- Build queries
- Implement business rules
They simply orchestrate communication between Express and the Service layer.
The Service layer contains the heart of the application.
This is where all business logic lives.
Examples include:
- URL shortening
- Duplicate detection
- Cache lookup
- Cache update
- Expiration handling
- Short code generation
- Business validation
The Service layer does not know anything about Express.
Instead, it focuses entirely on application behavior.
Keeping business logic isolated allows the same code to be reused from different interfaces such as REST APIs, CLI tools, scheduled jobs, or future GraphQL endpoints.
Repositories isolate all database operations.
Instead of querying MongoDB directly from controllers or services, database access is delegated to repository classes.
Responsibilities include:
- Find documents
- Create documents
- Update documents
- Delete documents
- Aggregate queries
Benefits include:
- Easier testing
- Better maintainability
- Database abstraction
- Cleaner business logic
If MongoDB is replaced in the future, only the repository layer requires modification.
models/
Contains all Mongoose schemas.
Each model represents a database collection.
Models define:
- Fields
- Types
- Validation
- Indexes
- Defaults
The model layer should only describe data structure.
Business logic belongs in the Service layer.
Validation is handled before requests reach controllers.
Using Joi schemas ensures that only valid data enters the application.
Responsibilities include:
- Input validation
- URL validation
- Payload constraints
- Required fields
- Data sanitization
Invalid requests are rejected immediately with descriptive error messages.
Middleware forms the application's processing pipeline.
Every request passes through one or more middleware components before reaching the controller.
Current middleware responsibilities include:
- Validation
- Rate limiting
- Advanced rate limiting
- Redis cache
- Error handling
This modular approach allows features to be added or removed without changing controller logic.
Contains Swagger configuration.
Swagger automatically generates interactive API documentation, allowing developers to explore and test endpoints directly from the browser.
Benefits include:
- Interactive API testing
- Request examples
- Response schemas
- Faster onboarding
Contains static assets served directly by Express.
Current assets include:
- HTML
- CSS
This folder can later be extended to include client-side JavaScript, images, or documentation assets.
The utility layer contains reusable helper functions shared across the application.
Examples include:
- Base62 encoding
- Hash generation
- Logger
- Response formatter
- Monitoring utilities
Utilities should remain stateless and generic.
Contains standalone scripts that are executed independently of the HTTP server.
Examples include:
- Cleanup jobs
- Performance testing
- Utility scripts
- Development helpers
Keeping scripts separate prevents unrelated logic from entering the main application.
The testing directory is organized by testing strategy rather than feature.
tests/
unit/
integration/
performance/
Validate individual functions in isolation.
Verify interactions between multiple layers of the application.
Measure caching performance, request throughput, and system efficiency.
This separation provides better visibility into the application's quality and performance.
Every incoming request follows the same predictable lifecycle.
Incoming HTTP Request
β
βΌ
Express Application
β
βΌ
Route Resolution
β
βΌ
Validation Middleware
β
βΌ
Rate Limiter
β
βΌ
Cache Middleware (when applicable)
β
βΌ
Controller
β
βΌ
Service
β
βΌ
Repository
β
βΌ
MongoDB
β
βΌ
Repository
β
βΌ
Service
β
βΌ
Controller
β
βΌ
Response Formatter
β
βΌ
HTTP Response
Every layer has a clearly defined responsibility, making the execution path deterministic, easy to debug, and simple to extend.
This project follows several architectural principles that guide its implementation:
- Single Responsibility Principle (SRP): Each module performs one well-defined task.
- Separation of Concerns: Routing, validation, business logic, persistence, and configuration remain independent.
- Layered Architecture: Requests pass through well-defined layers with minimal coupling.
- Repository Pattern: Database operations are isolated from business logic.
- Service Layer Pattern: Business rules are centralized and reusable.
- Configuration over Hardcoding: Environment variables and centralized configuration keep deployments flexible.
- Developer Experience: Consistent project organization, logging, testing, and documentation make onboarding and maintenance straightforward.
By following these principles, the project remains scalable, testable, and ready for future enhancements such as authentication, analytics, custom domains, or distributed deployments.
Whether you're looking to contribute, explore the architecture, or deploy the application for your own use, this guide will help you get up and running quickly.
The project supports two development workflows:
- Local Development using Node.js, MongoDB, and Redis installed on your machine.
- Containerized Development using Docker and Docker Compose (recommended).
Docker provides the most consistent development experience by ensuring every contributor uses the same runtime environment.
Before running the project, ensure the following software is installed.
- Node.js (v18 or higher)
- npm
- MongoDB
- Redis
- Docker
- Docker Compose
Verify your installation:
docker --version
docker compose version
node -v
npm -vgit clone https://github.com/<your-username>/URLshortener.git
cd URLshortenerCreate a local environment file.
cp .env.example .envConfigure the required variables.
Example:
NODE_ENV=development
PORT=3000
BASE_URL=http://localhost:3000
MONGODB_URI=mongodb://mongodb:27017/urlshortener
REDIS_HOST=redis
REDIS_PORT=6379Depending on your deployment strategy, additional variables may be added over time. The .env.example file should always represent the minimum required configuration.
Docker Compose starts the complete application stack, including:
- Node.js Application
- MongoDB
- Redis
Build the containers:
docker compose buildStart the services:
docker compose upRun in detached mode:
docker compose up -dStop the application:
docker compose downRebuild after code changes:
docker compose up --build Docker Compose
ββββββββββββββββββββββββββ
β β
β Node.js Container β
β β
ββββββββββββ¬ββββββββββββββ
β
ββββββββββββ΄ββββββββββββββ
β β
βΌ βΌ
MongoDB Container Redis Container
β β
ββββββββββββββββ¬ββββββββββ
β
Application
Each service runs independently while communicating over Docker's internal network.
This architecture mirrors a simplified production deployment and keeps dependencies isolated from the host machine.
Install dependencies:
npm installStart MongoDB.
Start Redis.
Then launch the application:
npm run devor
npm startThe server should now be accessible at:
http://localhost:3000
Interactive API documentation is powered by Swagger.
Once the server is running, open the Swagger interface in your browser to explore available endpoints, inspect request/response schemas, and test APIs directly.
Swagger provides:
- Endpoint descriptions
- Request examples
- Response schemas
- Error responses
- Interactive testing
This makes integration and development significantly easier.
The project uses Winston for structured logging.
Logs provide valuable insight into:
- Server startup
- Request lifecycle
- Errors
- Warnings
- Debug information
- Unexpected failures
Centralized logging makes troubleshooting easier and prepares the application for future integration with monitoring platforms such as ELK Stack, Grafana, or cloud logging services.
Redis acts as the application's high-speed caching layer.
Instead of querying MongoDB for every redirect request, frequently accessed URLs can be served directly from Redis.
Benefits include:
- Lower response latency
- Reduced database load
- Improved scalability
- Better user experience
This caching strategy becomes increasingly valuable as traffic grows.
To protect the service from abuse, multiple rate limiting strategies are implemented.
Current protections include:
- Request throttling
- Abuse prevention
- Burst protection
- Configurable request limits
Rate limiting improves both application stability and resource utilization.
Incoming requests are validated before entering the business layer.
Validation ensures:
- Required fields are present
- Invalid URLs are rejected
- Malformed requests never reach the database
- Error responses remain consistent
By validating at the application's edge, unnecessary processing and database operations are avoided.
The repository includes multiple testing strategies.
tests/
βββ unit
βββ integration
βββ performance
Run all tests:
npm testRun specific test suites as needed according to your project scripts.
Comprehensive testing helps ensure application correctness while preventing regressions during development.
The project is actively designed to evolve over time.
Potential future improvements include:
- JWT Authentication
- API Key Authentication
- User Accounts
- Custom Short URLs
- URL Analytics Dashboard
- Click Tracking
- Geographic Analytics
- QR Code Generation
- Expiration Policies
- Background Cleanup Workers
- Distributed Caching
- Nginx Reverse Proxy
- CI/CD Pipeline
- GitHub Actions
- Kubernetes Deployment
- Cloud Deployment Guides
- Prometheus Metrics
- Grafana Dashboards
- OpenTelemetry Integration
Contributions toward these features are always welcome.
- Verify MongoDB is running.
- Check the connection string in
.env. - Ensure Docker containers are healthy if using Docker.
- Verify Redis is running.
- Confirm host and port values.
- Restart the Redis container if necessary.
Try rebuilding the containers:
docker compose down
docker compose build --no-cache
docker compose upAnother process may already be using the configured port.
Either stop the conflicting process or update the port number in your environment configuration.
For production deployments, consider the following enhancements:
- Deploy behind a reverse proxy such as Nginx.
- Enable HTTPS using TLS certificates.
- Use environment-specific configuration.
- Store secrets securely using a secret manager.
- Enable database authentication.
- Configure Redis persistence if required.
- Add centralized monitoring and alerting.
- Implement health check endpoints.
- Introduce CI/CD pipelines for automated deployments.
- Schedule regular backups for MongoDB.
These practices improve security, reliability, and operational visibility.
Contributions of all kinds are welcome.
Whether you're fixing a bug, improving documentation, optimizing performance, or introducing a new feature, your efforts are greatly appreciated.
- Fork this repository.
- Clone your fork locally.
- Create a new feature branch.
git checkout -b feature/amazing-feature- Make your changes.
- Commit with clear, descriptive messages.
git commit -m "Add Redis cache optimization"- Push your branch.
git push origin feature/amazing-feature- Open a Pull Request with a clear explanation of the changes and the motivation behind them.
Please ensure that your code follows the project's style guidelines, passes existing tests, and includes additional tests where appropriate.
Constructive discussions, bug reports, and feature suggestions are equally valuable.
This project is distributed under the terms of the MIT License.
You are free to use, modify, and distribute the software in accordance with the license terms. See the LICENSE file for complete details.
This project draws inspiration from production backend architectures and modern software engineering practices, emphasizing maintainability, scalability, and clean code organization.
Special thanks to the open-source community for the incredible tools and libraries that make projects like this possible.
If you encounter an issue, have a question, or would like to propose an enhancement, please open an issue in the repository.
Meaningful bug reports, feature requests, and thoughtful discussions help improve the project for everyone.
Open source thrives because developers from around the world choose to learn, share, and build together.
If this project helped you understand backend architecture, taught you something new, or saved you development time, consider giving it a β on GitHub. It helps others discover the project and motivates continued development.
Every contributionβwhether it's code, documentation, testing, performance improvements, or simply reporting a bugβmakes the project stronger.
If you'd like to contribute, don't hesitate to fork the repository, experiment with new ideas, and submit a pull request. Every improvement, no matter how small, is appreciated.
Thank you for taking the time to explore this project.
Happy Coding! π