A full‑stack URL shortener built with Node.js, Drizzle ORM, and Docker — featuring clean API routes, database migrations, and modular services.
- Features
- Tech Stack
- Prerequisites
- Setup Instructions
- API Endpoints
- Folder Structure
- Database Schema
- Development
- License
- 🔗 Shorten long URLs into compact, shareable links
- 🛡️ Input validation and error handling via custom middleware
- 🗃️ Database migrations and schema management using Drizzle ORM
- 🧩 Modular architecture with services, routes, and models
- 🚀 Dockerized setup for easy deployment and scalability
- 📊 Analytics-ready structure for tracking URL usage
- ⚡ Lightning-fast redirects with optimized database queries
- 🔒 Environment-based configuration for security
Backend
- Node.js (v18+)
- Express.js (REST API)
- Drizzle ORM (type-safe database toolkit)
Database
- PostgreSQL (relational database)
Containerization
- Docker
- Docker Compose
Package Management
- pnpm (fast, disk-efficient package manager)
Before you begin, ensure you have the following installed:
- Node.js (v18 or higher) — Download
- pnpm (v8 or higher) — Install via
npm install -g pnpm - Docker and Docker Compose — Get Docker
- Git — Download
git clone https://github.com/PETROL-BOY/URL_SHORTNER.git
cd URL_SHORTNERpnpm installCreate a .env file in the root directory:
cp .env.example .envUpdate the .env file with your configuration:
# Database Configuration
DATABASE_URL=postgresql://postgres:password@localhost:5432/url_shortener
# Server Configuration
PORT=3000
NODE_ENV=development
# Application Settings
BASE_URL=http://localhost:3000
SHORT_URL_LENGTH=6Using Docker (Recommended):
docker-compose up -d dbUsing Local PostgreSQL:
CREATE DATABASE url_shortener;pnpm db:migrateDevelopment Mode:
pnpm devUsing Docker Compose (Full Stack):
docker-compose up --buildThe application will be available at: http://localhost:3000
POST /api/shorten
Create a new shortened URL.
Request Body:
{
"originalUrl": "https://www.example.com/very/long/url/path"
}Response (201 Created):
{
"success": true,
"data": {
"id": "abc123",
"originalUrl": "https://www.example.com/very/long/url/path",
"shortUrl": "http://localhost:3000/abc123",
"createdAt": "2025-11-19T12:51:00.000Z"
}
}Error Response (400 Bad Request):
{
"success": false,
"error": "Invalid URL format"
}GET /:shortCode
Redirect to the original URL using the short code.
Example:
curl http://localhost:3000/abc123
# Redirects to: https://www.example.com/very/long/url/pathResponse:
- 302 Found (redirect to original URL)
- 404 Not Found (if short code doesn't exist)
GET /api/url/:shortCode
Retrieve details about a shortened URL without redirecting.
Response (200 OK):
{
"success": true,
"data": {
"id": "abc123",
"originalUrl": "https://www.example.com/very/long/url/path",
"shortUrl": "http://localhost:3000/abc123",
"clicks": 42,
"createdAt": "2025-11-19T12:51:00.000Z"
}
}GET /health
Check if the server is running.
Response (200 OK):
{
"status": "ok",
"timestamp": "2025-11-19T12:51:00.000Z"
}URL_SHORTNER/
│
├── db/ # Database configuration and migrations
│ ├── drizzle/ # Generated migration files
│ ├── schema.js # Drizzle schema definitions
│ └── index.js # Database connection setup
│
├── middlewares/ # Custom middleware functions
│ ├── errorHandler.js # Global error handling
│ ├── validateUrl.js # URL validation middleware
│ └── rateLimiter.js # Rate limiting
│
├── models/ # Data models and entity definitions
│ └── url.model.js # URL entity model
│
├── routes/ # API route handlers
│ ├── index.js # Main router
│ ├── shorten.routes.js # URL shortening endpoints
│ └── redirect.routes.js # Redirect handler
│
├── services/ # Business logic layer
│ ├── url.service.js # URL shortening logic
│ ├── analytics.service.js # Click tracking
│ └── shortCode.service.js # Short code generation
│
├── utils/ # Helper functions and utilities
│ ├── generateShortCode.js # Random short code generator
│ ├── validateUrl.js # URL validation helper
│ └── logger.js # Logging utility
│
├── validation/ # Input validation schemas
│ └── url.validation.js # URL validation rules
│
├── view/ # Static files or frontend templates
│ ├── index.html # Landing page
│ └── assets/ # CSS, JS, images
│
├── .env.example # Sample environment configuration
├── .gitignore # Git ignore rules
├── docker-compose.yml # Docker multi-container setup
├── Dockerfile # Application container definition
├── drizzle.config.js # Drizzle ORM configuration
├── index.js # Application entry point
├── package.json # Project metadata and scripts
├── pnpm-lock.yaml # Dependency lock file
└── README.md # Project documentation
The application uses the following database schema with Drizzle ORM:
// db/schema.js
import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
export const urls = pgTable('urls', {
id: serial('id').primaryKey(),
shortCode: text('short_code').notNull().unique(),
originalUrl: text('original_url').notNull(),
clicks: integer('clicks').default(0),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});Table: urls
| Column | Type | Description |
|---|---|---|
| id | SERIAL | Primary key (auto-increment) |
| shortCode | TEXT | Unique short code (e.g., "abc123") |
| originalUrl | TEXT | Original long URL |
| clicks | INTEGER | Number of times URL was accessed |
| createdAt | TIMESTAMP | Creation timestamp |
| updatedAt | TIMESTAMP | Last update timestamp |
# Start development server with hot reload
pnpm dev
# Start production server
pnpm start
# Run database migrations
pnpm db:migrate
# Generate new migrations (after schema changes)
pnpm db:generate
# Open Drizzle Studio (database GUI)
pnpm db:studio
# Run tests
pnpm test- Create a new route in
routes/ - Add business logic in
services/ - Update validation schemas in
validation/ - Add middleware if needed in
middlewares/ - Update database schema in
db/schema.jsand runpnpm db:generate
- Use ES6+ syntax (const, arrow functions, destructuring)
- Follow RESTful API conventions
- Write descriptive commit messages
- Add error handling for all async operations
- Use environment variables for configuration
Build and run the entire stack:
docker-compose up --build -dStop the containers:
docker-compose downView logs:
docker-compose logs -fThis project is licensed under the MIT License.
MIT License
Copyright (c) 2025 PETROL-BOY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
If you encounter any issues or have questions:
- Open an issue: GitHub Issues
- Built with Drizzle ORM
- Powered by Node.js and Express
- Containerized with Docker
Made with ❤️ by PETROL-BOY