Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

tom-dorcely/code-exercise-java

Open more actions menu
 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
18 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

URL Shortener

A small URL shortener built for the coding exercise brief.

  • Backend — Spring Boot 3.3 / Java 21 with Spring MVC, Spring Data JPA and PostgreSQL.
  • Frontend — Next.js 16 (App Router) / React 19 with Tailwind v4.
  • Tests — every feature was developed test-first (JUnit 5, Mockito, @WebMvcTest, @DataJpaTest + Testcontainers; Vitest, React Testing Library + MSW).
  • Containerised — one docker compose up for db + api + ui.

Architecture at a glance

┌──────────────┐    HTTP/JSON    ┌──────────────┐    JDBC    ┌──────────────┐
│ Next.js UI   │ ──────────────► │ Spring Boot  │ ─────────► │ PostgreSQL   │
│ :3000        │                 │ :8080        │            │ :5432        │
└──────────────┘                 └──────────────┘            └──────────────┘

Backend layout (backend/src/main/java/com/example/urlshortener/):

Package Responsibility
domain Value objects (Alias, FullUrl) and the Base62AliasEncoder (id → alias). Pure Java, fully unit-tested.
persistence ShortUrlEntity + Spring Data ShortUrlRepository, plus the short_url_id_seq sequence in schema.sql.
application ShortUrlService — implements the shorten flow (dedup → next id → encode → save), plus custom-alias guard and find/list/delete.
web DTOs, UrlController, GlobalExceptionHandler, ApiError.
config AppProperties (app.*), ApplicationConfig, CorsConfig.

Alias generation

 1. input: longURL
 2. longURL in DB?  ─yes─►  3. return existing shortURL
          │ no
          ▼
 4. nextval('short_url_id_seq')   ──►   5. base62-encode the id   ──►   6. save (id, alias, longURL)

A monotonic database sequence feeds a bijective base62 encoder, so two distinct ids always produce two distinct aliases — random collisions between auto-generated aliases are eliminated by construction. Custom aliases share the same alias column (protected by a unique index); on the rare chance an encoded id happens to shadow an existing custom alias, the service walks forward in the sequence until it finds a free slot.

API

Implements openapi.yaml. All endpoints are JSON.

Method Path Status codes Notes
POST /shorten 201, 400 Body: { "fullUrl": "...", "customAlias": "..." } (alias optional)
GET /{alias} 302 (Location), 404, 400 Public redirect endpoint
DELETE /{alias} 204, 404, 400
GET /urls 200 Returns [{ alias, fullUrl, shortUrl }]

Validation rules:

  • fullUrl must be http/https, ≤ 2048 chars, with a host.
  • customAlias (and any path alias) must match ^[A-Za-z0-9_-]+$, ≤ 64 chars, and is not one of the reserved names (shorten, urls).
  • A duplicate custom alias yields 400 with Alias '...' is already taken.
  • A blank customAlias is treated as "no alias" and a random one is generated.

Errors use a uniform envelope:

{
  "timestamp": "2026-05-03T18:00:00Z",
  "status": 400,
  "error": "Bad Request",
  "message": "URL must use the http or https scheme"
}

Quick start (Docker)

Prereqs: Docker + Docker Compose.

docker compose up --build

When everything is healthy:

The frontend image is built with NEXT_PUBLIC_API_BASE_URL=http://localhost:8080. To point it at a different backend, override that build arg in docker-compose.yml (or rebuild with docker compose build --build-arg NEXT_PUBLIC_API_BASE_URL=...).

Local development (without Docker)

1. Start PostgreSQL

docker run --rm -d --name url-shortener-db \
  -e POSTGRES_USER=urlshortener \
  -e POSTGRES_PASSWORD=urlshortener \
  -e POSTGRES_DB=urlshortener \
  -p 5432:5432 postgres:16-alpine

2. Run the backend

cd backend
./mvnw spring-boot:run    # or: mvn spring-boot:run

Configurable via env (defaults shown):

Env var Default
APP_PUBLIC_BASE_URL http://localhost:8080
APP_CORS_ALLOWED_ORIGINS http://localhost:3000
SPRING_DATASOURCE_URL jdbc:postgresql://localhost:5432/urlshortener
SPRING_DATASOURCE_USERNAME urlshortener
SPRING_DATASOURCE_PASSWORD urlshortener
SPRING_JPA_HIBERNATE_DDL_AUTO update

3. Run the frontend

cd frontend
npm install
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 npm run dev

Open http://localhost:3000.

Running the tests

Backend

cd backend
mvn test

What runs:

  • Pure unitAliasTest, FullUrlTest, Base62AliasEncoderTest, ShortUrlServiceTest (Mockito).
  • Web sliceUrlControllerTest, CorsConfigTest (@WebMvcTest).
  • Persistence sliceShortUrlRepositoryTest (@DataJpaTest against a Testcontainers Postgres).
  • Full contextUrlShortenerApplicationTests (@SpringBootTest).

The repository / Spring-context tests need a Docker daemon (Testcontainers spins up postgres:16-alpine).

Frontend

cd frontend
npm test            # vitest run, headless
npm run test:watch  # watch mode
npm run lint        # eslint

Covers the typed API client (with MSW), the ShortenForm and UrlList components, and the HomeView orchestrator (load → submit → refresh, error propagation, delete).

Example usage

From the UI

  1. Open http://localhost:3000.
  2. Paste a URL into "Full URL" (e.g. https://nextjs.org/docs).
  3. (Optional) type a custom alias like docs.
  4. Click Shorten — the new entry appears in the list with a copyable short URL.
  5. Click Delete to remove it; click the short URL to follow the redirect.

From the API

# Create with an auto-generated alias (id from short_url_id_seq, base62-encoded)
curl -i -X POST http://localhost:8080/shorten \
  -H 'Content-Type: application/json' \
  -d '{"fullUrl":"https://nextjs.org/docs"}'
# HTTP/1.1 201
# {"shortUrl":"http://localhost:8080/B"}      # for id=1; later ids grow as needed

# Create with a custom alias
curl -i -X POST http://localhost:8080/shorten \
  -H 'Content-Type: application/json' \
  -d '{"fullUrl":"https://nextjs.org/docs","customAlias":"docs"}'
# HTTP/1.1 201
# {"shortUrl":"http://localhost:8080/docs"}

# Follow the redirect
curl -i http://localhost:8080/docs
# HTTP/1.1 302
# Location: https://nextjs.org/docs

# List all
curl http://localhost:8080/urls

# Delete
curl -i -X DELETE http://localhost:8080/docs
# HTTP/1.1 204

Notes & assumptions

  • Persistence — chose Postgres over an embedded H2 so the deployed shape matches what tests run against (Testcontainers). The table is auto-managed by Hibernate (ddl-auto=update); the short_url_id_seq sequence is created by schema.sql (idempotent CREATE SEQUENCE IF NOT EXISTS). A real deployment would use Flyway/Liquibase to manage both.
  • Alias generation — a database sequence (short_url_id_seq) feeds a bijective base62 encoder, so distinct ids always produce distinct aliases. Auto-vs-auto random collisions are mathematically impossible. The custom-alias path is a separate guard that returns 400 immediately if the alias is taken; if a future encoded id ever shadows an existing custom alias, the service walks forward in the sequence until it finds a free slot.
  • Dedup on the auto-alias path — shortening the same URL twice without a custom alias returns the existing entry instead of creating a second one. The custom-alias path skips this on purpose: if you explicitly ask for /foo, you get /foo (or 400 if it's taken), even if the URL is already shortened under another alias.
  • Reserved aliasesshorten and urls are blocked at the Alias value object level so they can't shadow API routes.
  • Error modelIllegalArgumentException from value objects and AliasAlreadyTakenException both map to 400 with the exception message in the response body, so the UI can show it verbatim.
  • CORS — driven by app.cors-allowed-origins. Defaults to http://localhost:3000 for local dev; in compose it's set to the same.
  • Reading customAlias = " " — treated as "no alias provided" both in the controller and the API client, so a stray whitespace from the form doesn't break shortening.
  • Frontend state — kept inside HomeView (no global store) since the page has a single concern. Initial load lives in an async IIFE inside useEffect to keep React 19's set-state-in-effect lint rule happy.
  • Time spent — roughly half a day (≈4 hours) end-to-end across backend, frontend, tests, dockerisation and docs.

Exercise brief

The original task description is preserved below for context.

Task

Build a simple URL shortener in a preferably JVM-based language (e.g. Java, Kotlin).

It should:

  • Accept a full URL and return a shortened URL.
  • A shortened URL should have a randomly generated alias.
  • Allow a user to customise the shortened URL if they want to (e.g. user provides my-custom-alias instead of a random string).
  • Persist the shortened URLs across restarts.
  • Expose a decoupled web frontend built with a modern framework (e.g., React, Next.js, Vue.js, Angular, Flask with templates).
  • Expose a RESTful API to perform create/read/delete operations on URLs (per openapi.yaml).
  • Include the ability to delete a shortened URL via the API.
  • Have tests.
  • Be containerised (e.g. Docker).
  • Include instructions for running locally.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Java 63.1%
  • TypeScript 34.1%
  • Dockerfile 1.6%
  • Other 1.2%
Morty Proxy This is a proxified and sanitized view of the page, visit original site.