Skip to content

Karan-Rastogi/URL-Shortener-backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

URL Shortener — Phase 1

A production-ready URL shortening backend built with Spring Boot, MySQL, and JWT authentication. Designed with SOLID principles throughout.


Tech Stack

Layer Technology
Language Java 17
Framework Spring Boot 3.2.x
Database MySQL 8.x
ORM Spring Data JPA + Hibernate
Security Spring Security + JWT (jjwt 0.12.3)
Build Tool Maven
Utilities Lombok, Validation

Project Structure

src/main/java/com/example/URLShortner/
├── config/
│   └── SecurityConfig.java          # Spring Security config, JWT filter chain
├── controller/
│   ├── AuthController.java          # Register + login endpoints
│   ├── UrlController.java           # Shorten, redirect, list, delete endpoints
│   └── GlobalExceptionHandler.java  # Centralized error handling
├── dto/
│   ├── request/
│   │   ├── RegisterRequest.java     # Email + password input
│   │   ├── LoginRequest.java        # Email + password input
│   │   └── ShortenRequest.java      # URL + optional alias + optional expiry
│   └── response/
│       ├── AuthResponse.java        # JWT token + user info
│       ├── UrlResponse.java         # Short URL details
│       └── ErrorResponse.java       # Standardized error shape
├── model/
│   ├── User.java                    # users table entity
│   └── Url.java                     # urls table entity
├── repository/
│   ├── UserRepository.java          # User DB access
│   └── UrlRepository.java           # URL DB access
├── scheduler/
│   └── UrlCleanupScheduler.java     # Nightly expired URL cleanup
├── security/
│   ├── JwtUtil.java                 # Token generation + validation
│   ├── JwtFilter.java               # Intercepts every request
│   └── UserDetailsServiceImpl.java  # Loads user from DB for Spring Security
├── service/
│   ├── AuthService.java             # Auth interface (OCP)
│   ├── UrlService.java              # URL interface (OCP)
│   └── impl/
│       ├── AuthServiceImpl.java     # Register + login logic
│       └── UrlServiceImpl.java      # Shorten + redirect + manage logic
└── util/
    └── Base62Encoder.java           # Converts auto-increment ID → short code

Database Schema

users table

Column Type Constraints
id BIGINT PK, AUTO_INCREMENT
email VARCHAR(100) NOT NULL, UNIQUE
password VARCHAR(255) NOT NULL (BCrypt hashed)
role ENUM(USER, ADMIN) NOT NULL
created_at DATETIME NOT NULL
is_enabled TINYINT(1) NOT NULL, DEFAULT 1

urls table

Column Type Constraints
id BIGINT PK, AUTO_INCREMENT
short_code VARCHAR(20) UNIQUE
long_url VARCHAR(2048) NOT NULL
user_id BIGINT FK → users(id), NULLABLE
expires_at DATETIME NULLABLE
is_active TINYINT(1) NOT NULL, DEFAULT 1
click_count BIGINT NOT NULL, DEFAULT 0
created_at DATETIME NOT NULL
custom_alias VARCHAR(50) UNIQUE, NULLABLE

API Endpoints

Auth — /api/auth

Method Endpoint Auth Description
POST /api/auth/register Public Create account, returns JWT
POST /api/auth/login Public Login, returns JWT

URLs — /api/urls

Method Endpoint Auth Description
POST /api/urls/shorten Public Shorten a URL
GET /api/urls/my Required Get all my URLs
DELETE /api/urls/{shortCode} Required Soft delete a URL

Redirect

Method Endpoint Auth Description
GET /{shortCode} Public Redirect to original URL

SOLID Principles Applied

S — Single Responsibility

Each class has exactly one job. AuthController only handles HTTP routing. AuthServiceImpl only handles auth logic. Base62Encoder only encodes numbers to short codes. No class mixes responsibilities.

O — Open/Closed

AuthService and UrlService are interfaces. Controllers depend only on interfaces — never on concrete implementations. To add a new auth strategy (e.g. OAuth), create a new class implementing AuthService without touching the controller.

L — Liskov Substitution

Any implementation of AuthService or UrlService is safely swappable. The controller works identically regardless of which implementation Spring injects. Demonstrated by the clean interface-to-implementation separation throughout.


How Base62 Encoding Works

Every URL row gets an auto-increment integer ID from MySQL. That ID is encoded to Base62 to create the short code.

Characters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
Base: 62

id = 1       → "b"
id = 100     → "bM"
id = 100523  → "q7X"

The encoding is deterministic — same ID always produces the same code. Zero collision possible because MySQL guarantees unique auto-increment IDs.

This approach was chosen over random generation (collision risk at scale) and UUID (too long for a short URL).


JWT Authentication Flow

1. POST /api/auth/login with email + password
2. Server verifies BCrypt password hash
3. Server generates JWT: { subject: email, expiry: 24h }
4. Client stores token
5. Client sends: Authorization: Bearer <token>
6. JwtFilter intercepts every request
7. Validates token signature and expiry
8. Sets user in Spring Security context
9. Controller receives authenticated request

Tokens are stateless — the server stores nothing. Every request is self-contained.


URL Shortening Flow

POST /api/urls/shorten { longUrl: "https://google.com" }
        ↓
1. Check custom alias not taken (if provided)
2. Save Url row to MySQL with shortCode = null
3. MySQL returns auto-increment id (e.g. 1)
4. Base62 encode id → shortCode (e.g. "b")
5. Update row with shortCode
6. Return { shortUrl: "http://localhost:8080/b", ... }

GET /b
        ↓
1. Look up shortCode "b" in MySQL
2. Check is_active = true
3. Check not expired
4. Increment click_count
5. Return 302 redirect to "https://google.com"

Setup and Run

Prerequisites

  • Java 17+
  • MySQL 8.x
  • Maven 3.x

1. Create the database

CREATE DATABASE url_shortener_db;

2. Configure application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/url_shortener_db
spring.datasource.username=root
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

app.jwt.secret=404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
app.jwt.expiration-ms=86400000

app.base-url=http://localhost:8080

server.port=8080

3. Run

mvn spring-boot:run

App starts on http://localhost:8080


Testing with Postman

Register

POST http://localhost:8080/api/auth/register
Content-Type: application/json

{
  "email": "user@gmail.com",
  "password": "secret123"
}

Login

POST http://localhost:8080/api/auth/login
Content-Type: application/json

{
  "email": "user@gmail.com",
  "password": "secret123"
}

Shorten a URL

POST http://localhost:8080/api/urls/shorten
Content-Type: application/json

{
  "longUrl": "https://www.google.com",
  "customAlias": "google",
  "expiryDays": 30
}

Redirect

Open in browser: http://localhost:8080/google
→ Redirects to https://www.google.com

Get my URLs

GET http://localhost:8080/api/urls/my
Authorization: Bearer <token>

What's Coming — Phase 2

Feature Technology Why
URL caching Redis Redirect in ~3ms instead of ~50ms
Rate limiting Redis sliding window Prevent abuse
Click analytics Kafka + MongoDB Async processing, geo + device breakdown
Cleanup Enhanced scheduler Redis cache eviction on expiry

Key Design Decisions

Why soft delete instead of hard delete? Setting is_active = false instead of deleting the row preserves click history and allows recovery. A hard delete would lose all analytics data permanently.

Why save twice when shortening? MySQL must assign the auto-increment ID before we can Base62-encode it. First save creates the row and returns the ID. Second save updates the shortCode. This guarantees zero collisions without any random generation or UUID.

Why separate request and response DTOs? The User entity has sensitive fields like password and isEnabled that must never be exposed in API responses. DTOs give full control over what enters and exits the API layer.

Why JWT over sessions? REST APIs should be stateless. JWT tokens carry all auth information inside themselves — the server stores nothing between requests. This makes the app horizontally scalable — any server instance can validate any token.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages