A clean, production-grade URL shortener built with Java 17 + Spring Boot 3.2 (backend) and React + TypeScript + Vite (frontend), fully containerised with Docker.
| Layer | Technology |
|---|---|
| Backend | Java 17, Spring Boot 3.2, Spring Data JPA |
| Database | PostgreSQL 16 (prod) / H2 file (dev) |
| Frontend | React 18, TypeScript, Vite, plain CSS |
| Tests | JUnit 5 + Mockito (backend), Vitest + RTL (frontend) |
| Container | Docker multi-stage builds, Docker Compose, Maven |
- Docker & Docker Compose ≥ v2
- (Optional, for running tests without Docker) JDK 17+, Maven 3.9+, Node 20+
git clone <your-fork-url>
cd url-shortener
docker compose up --build- Frontend: http://localhost:3000
- Backend API: http://localhost:8080
docker compose -f docker-compose.dev.yml up --buildSame ports, data stored in an H2 file database inside the container volume.
cd backend
./mvnw spring-boot:runStarts on port 8080. Data persisted to ./data/urlshortener.mv.db.
cd frontend
npm install
npm run devStarts on http://localhost:3000. Proxies /shorten, /urls, and /{alias} to http://localhost:8080.
cd backend
./mvnw testTests include:
- Unit tests (
UrlShortenerServiceTest) — alias generation, all validation branches, CRUD operations - Integration tests (
UrlShortenerControllerIntegrationTest) — full HTTP round-trips via MockMvc against an in-memory H2 database
cd frontend
npm install
npm testmake testFull spec in openapi.yaml. Quick examples:
curl -s -X POST http://localhost:8080/shorten \
-H "Content-Type: application/json" \
-d '{"fullUrl": "https://example.com/a/very/long/path"}'
# → {"shortUrl":"http://localhost:8080/xK3mP7a"}curl -s -X POST http://localhost:8080/shorten \
-H "Content-Type: application/json" \
-d '{"fullUrl": "https://example.com", "customAlias": "my-link"}'
# → {"shortUrl":"http://localhost:8080/my-link"}curl -v http://localhost:8080/my-link
# → 302 Location: https://example.comcurl -s http://localhost:8080/urls | jq .curl -X DELETE http://localhost:8080/my-link
# → 204 No Content# Invalid URL scheme
curl -s -X POST http://localhost:8080/shorten \
-H "Content-Type: application/json" \
-d '{"fullUrl": "ftp://example.com"}'
# → 400 {"error": "URL must start with http:// or https://"}
# Duplicate alias
curl -s -X POST http://localhost:8080/shorten \
-H "Content-Type: application/json" \
-d '{"fullUrl": "https://other.com", "customAlias": "my-link"}'
# → 400 {"error": "Alias 'my-link' is already taken"}ShortenedUrl— JPA entity;aliasis the natural primary key (no surrogate needed)UrlShortenerService— all business logic: URL validation viajava.net.URI, alias regex validation,SecureRandom-based alias generation with collision retry up to 10 attemptsUrlShortenerController— thin HTTP adapter with Java records for request/response DTOs; matches OpenAPI contract exactlyGlobalExceptionHandler—@RestControllerAdvicemaps domain exceptions to400/404/500with consistent{"error": "..."}bodiesUrlShortenerExceptions— domain exceptions as static inner classes, keeping the package clean
- Records for
ShortenRequest,ShortenResponse,UrlEntry,ErrorResponse— immutable DTOs with no boilerplate - Text blocks in integration tests for inline JSON
List.of(),stream().toList()— modern collections API
Random aliases are 7 characters from [a-zA-Z0-9] using SecureRandom. On collision it retries up to 10 times. Custom aliases accept [a-zA-Z0-9_-] up to 50 characters.
- Dev / H2: File-based H2, zero external dependencies. Schema at
./data/ - Prod / Postgres: Full ACID persistence. Schema managed by Hibernate
ddl-auto: update
make up # Full stack (Postgres)
make dev # Lightweight (H2)
make down # Stop and remove containers
make test # Run all tests
make test-backend # Backend only (mvn test)
make test-frontend # Frontend only (vitest)
make logs # Follow container logs
make clean # Remove everything including volumes- No auth — out of scope; production would add API keys or JWT.
- H2 default for local dev — eliminates needing a local Postgres for
mvn spring-boot:run. - Java records for DTOs — clean, immutable, zero boilerplate (Java 17+).
SecureRandomoverRandom— better entropy for alias generation; still fast enough for this use case.BASE_URLenv var — makes the deployment URL configurable without code changes.- CORS
*— fine for a demo; production would restrict to the frontend origin.
Approximately 3–4 hours including project scaffolding, backend + tests, frontend + tests, Docker, and README.