Skip to content

hussein-hub/url-shortener

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

10 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”— URL Shortener

A modern, full-stack URL shortening service built with Spring Boot and deployed on Render. Transform long URLs into short, shareable links with click tracking and custom code support.

Java Spring Boot PostgreSQL License

⚠️ Note: The deployed service may experience latency (30-60 seconds on first request) due to cold starts and slower VMs on Render's free tier. The application spins down after 15 minutes of inactivity. Subsequent requests after the initial wake-up are fast.

✨ Features

  • πŸš€ Lightning Fast - Instant URL shortening with optimized backend
  • πŸ“Š Click Tracking - Monitor how many times your links are clicked
  • 🎨 Custom Short Codes - Create memorable links with personalized codes
  • πŸ”’ Secure - Environment-based configuration for sensitive data
  • 🌐 Responsive UI - Beautiful, mobile-friendly interface built with Tailwind CSS
  • ⚑ Auto-generated Codes - 6-character alphanumeric codes for quick sharing

πŸ› οΈ Tech Stack

Backend

  • Spring Boot 4.0.1 - Modern Java framework
  • Spring Data JPA - Database interaction
  • Hibernate - ORM with PostgreSQL dialect
  • Maven - Dependency management
  • Lombok - Reduce boilerplate code

Frontend

  • HTML5 / CSS3 - Semantic markup and styling
  • Tailwind CSS - Utility-first CSS framework
  • JavaScript (Vanilla) - Dynamic interactions
  • Font Awesome - Icon library

Database & Deployment

  • PostgreSQL (Supabase) - Cloud-hosted database
  • Render - Backend deployment platform
  • HikariCP - Connection pooling

πŸ—οΈ Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              Client Layer                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚   Browser    β”‚          β”‚    Mobile    β”‚         β”‚   Desktop    β”‚    β”‚
β”‚  β”‚  (HTML/CSS)  β”‚          β”‚     App      β”‚         β”‚     App      β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚         β”‚                         β”‚                        β”‚            β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                   β”‚                                     β”‚
β”‚                            HTTP/HTTPS (REST API)                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Application Layer (Render)                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚                     Spring Boot Application                    β”‚     β”‚
β”‚  β”‚                                                                β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚     β”‚
β”‚  β”‚  β”‚   Static     β”‚  β”‚     REST     β”‚  β”‚   Redirect   β”‚          β”‚     β”‚
β”‚  β”‚  β”‚   Content    β”‚  β”‚  Controller  β”‚  β”‚  Controller  β”‚          β”‚     β”‚
β”‚  β”‚  β”‚ (index.html) β”‚  β”‚   (/api/*)   β”‚  β”‚    (/s/*)    β”‚          β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚     β”‚
β”‚  β”‚         β”‚                  β”‚                  β”‚                β”‚     β”‚
β”‚  β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β”‚     β”‚
β”‚  β”‚                            β”‚                                   β”‚     β”‚
β”‚  β”‚                     β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”                             β”‚     β”‚
β”‚  β”‚                     β”‚            β”‚                             β”‚     β”‚
β”‚  β”‚                     β”‚  Service   β”‚                             β”‚     β”‚
β”‚  β”‚                     β”‚   Layer    β”‚                             β”‚     β”‚
β”‚  β”‚                     β”‚            β”‚                             β”‚     β”‚
β”‚  β”‚                     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                             β”‚     β”‚
β”‚  β”‚                            β”‚                                   β”‚     β”‚
β”‚  β”‚                     β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”                            β”‚     β”‚
β”‚  β”‚                     β”‚  JPA/       β”‚                            β”‚     β”‚
β”‚  β”‚                     β”‚  Hibernate  β”‚                            β”‚     β”‚
β”‚  β”‚                     β”‚  Repository β”‚                            β”‚     β”‚
β”‚  β”‚                     β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                            β”‚     β”‚
β”‚  β”‚                            β”‚                                   β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚                               β”‚                                         β”‚
β”‚                        β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”                                 β”‚
β”‚                        β”‚   HikariCP   β”‚                                 β”‚
β”‚                        β”‚ (Connection  β”‚                                 β”‚
β”‚                        β”‚    Pool)     β”‚                                 β”‚ 
β”‚                        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜                                 β”‚
β”‚                               β”‚                                         β”‚
β”‚                      JDBC over SSL/TLS                                  β”‚
└───────────────────────────────┼──────────────────────────────────────── β”˜
                                β”‚
                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Persistence Layer (Supabase)                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚  β”‚              PostgreSQL Database (EU Region)                   β”‚     β”‚
β”‚  β”‚                                                                β”‚     β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”‚     β”‚
β”‚  β”‚  β”‚  Table: url                                          β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”‚ id (BIGSERIAL PK)                            β”‚    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”‚ original_url (VARCHAR 2048)                  β”‚    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”‚ short_code (VARCHAR 10 UNIQUE)               β”‚    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”‚ click_count (BIGINT)                         β”‚    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β”‚ created_at (TIMESTAMP)                       β”‚    β”‚      β”‚     β”‚
β”‚  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚      β”‚     β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚     β”‚
β”‚  β”‚                                                                β”‚     β”‚
β”‚  β”‚  Features: Connection Pooler (Port 6543)                       β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Components:
β”œβ”€β”€ Frontend: Vanilla JS + Tailwind CSS (SPA served as static content)
β”œβ”€β”€ Backend: Spring Boot 4.0.1 with embedded Tomcat
β”œβ”€β”€ API: RESTful endpoints (POST /api/shorten, GET /api/info/:code)
β”œβ”€β”€ Redirect: GET /s/:code β†’ 302 redirect to original URL
β”œβ”€β”€ ORM: Hibernate with Spring Data JPA repositories
β”œβ”€β”€ Connection Pool: HikariCP (max 5 connections, min idle 2)
└── Database: PostgreSQL 13+ hosted on Supabase (EU)

### Request Flow

1. **URL Shortening Flow:**
   - User submits URL via frontend form
   - AJAX POST request to `/api/shorten` with JSON payload
   - UrlController validates and forwards to UrlService
   - UrlService generates/validates short code
   - JPA Repository persists to PostgreSQL via HikariCP
   - Response returns shortened URL to frontend

2. **Redirect Flow:**
   - User clicks short URL (e.g., `https://app.onrender.com/s/abc123`)
   - GET request to `/s/abc123`
   - UrlController fetches original URL from UrlService
   - Click count incremented atomically
   - HTTP 302 redirect to original URL

3. **Info Retrieval Flow:**
   - API request to `/api/info/abc123`
   - Returns JSON with URL stats (clicks, creation date, original URL)

## πŸš€ Getting Started

### Prerequisites

- Java 21 or higher
- Maven 3.6+
- PostgreSQL database (or Supabase account)

### Local Development

1. **Clone the repository**
```bash
git clone https://github.com/yourusername/url_shortener.git
cd url_shortener
  1. Create .env file
PORT=8080
APP_BASE_URL=http://localhost:8080
DB_URL=jdbc:postgresql://your-db-host:5432/postgres
DB_USERNAME=your_username
DB_PASSWORD=your_password
  1. Build the project
mvn clean install
  1. Run the application
mvn spring-boot:run
  1. Access the application
http://localhost:8080

🌍 Deployment

Render Deployment (Docker)

  1. Create a Dockerfile (if not exists)
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
  1. Push code to GitHub
git add .
git commit -m "Initial commit"
git push origin main
  1. Create a new Web Service on Render

    • Connect your GitHub repository
    • Select Docker as the environment
    • Select region (preferably EU to match Supabase)
    • Render will automatically detect the Dockerfile
    • No need to specify build or start commands (handled by Docker)
  2. Set Environment Variables

    • PORT - Automatically provided by Render
    • APP_BASE_URL - Your Render URL (e.g., https://your-app.onrender.com)
    • DB_URL - Your Supabase connection string (use pooler port 6543)
    • DB_USERNAME - Database username
    • DB_PASSWORD - Database password
  3. Deploy!

    • Render will build the Docker image and deploy your application
    • First deployment may take 2-5 minutes
    • Free tier instances spin down after 15 minutes of inactivity

Supabase Setup

  1. Create a new project on Supabase
  2. Navigate to Settings β†’ Database
  3. Copy the connection string (use Pooler for better performance with port 6543)
  4. Update your environment variables

πŸ“‘ API Endpoints

Shorten URL

POST /api/shorten
Content-Type: application/json

{
  "url": "https://example.com/very/long/url",
  "customCode": "mycode" // Optional
}

Response:

{
  "originalUrl": "https://example.com/very/long/url",
  "shortUrl": "https://your-app.onrender.com/s/mycode",
  "shortCode": "mycode",
  "createdAt": "2026-01-07T19:00:00Z",
  "clickCount": 0
}

Get URL Info

GET /api/info/{shortCode}

Redirect to Original URL

GET /s/{shortCode}

πŸ”§ Configuration

application.properties

# Database Configuration
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false

# Server Configuration
server.port=${PORT:8080}

πŸ“Š Database Schema

CREATE TABLE url (
    id BIGSERIAL PRIMARY KEY,
    original_url VARCHAR(2048) NOT NULL,
    short_code VARCHAR(10) UNIQUE NOT NULL,
    click_count BIGINT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

About

A simple URL shortener using Spring boot

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors