Simple URL Shortener API written in Ktor 🚀
- URL Shortening: Create short codes for long URLs
- Expiry Support: Set expiration dates for shortened URLs
- User Management: API key-based authentication
- Analytics: Track visit counts and top URLs
- Delete: URLs can be marked as deleted
- User Tiers:
HOBBYandENTERPRISEwith tier-based access (bulk is enterprise-only) - User Urls:
GET /urlsreturns a list of the user's shortened URLs with longUrls. - Bulk Shorten:
POST /bulk/shortenaccepts an array of requests and returns per-item results with a summary - Custom Short Code: provide
shortCodeon create/update to set your own code - Password Protection: optional
passwordrequired at redirect - ** Blacklist User**: Adds a user's API key to the blacklist.json file. Any API request using the same key will then be blocked from accessing any resources.
Test the API directly with Postman collection:
Or use Bruno API client with the collection in the bruno/ directory.
Run this command and server should be up, on port 8080
./gradlew clean runOr with docker compose:
docker compose up -dThis should return OK
curl 0.0.0.0:8080/status ./gradlew clean testOutput
> Task :test
ApplicationTest > testRoot PASSED
ApplicationTest > testShortenAndRedirect PASSED
Use the following test API keys via the x-api-key header:
- Alice:
sk_test_alice - Bob:
sk_test_bob
URLs can have expiration dates.
- Expired URLs return 404 on redirect attempts.
- Expiry can be updated via PUT requests.
- Expired URLs are automatically filtered.
Format: YYYY-MM-DDTHH:MM:SS (ISO 8601)
- Endpoints:
/shortenand/redirectAPIs - Environment: Docker Compose with resource constraints: 2 CPU cores and 4GB RAM
To run the benchmark:
k6 run benchmark/benchmark.js -u 400 -i 400 # concurrent users/iterations countCache performance benchmarks are available in the benchmark/ directory. Run benchmark/compare-cache-performance.sh
to compare performance with and without cache.
| Database | Concurrent users | p(50) | p(90) | p(95) | p(99) | failure % |
|---|---|---|---|---|---|---|
| In-memory | 10 | 6ms | 9.01ms | 9.93ms | 10.51ms | 0% |
| In-memory | 50 | 15.78ms | 28.39ms | 29.61ms | 36.99ms | 0% |
| In-memory | 100 | 18.84ms | 39.96ms | 40.37ms | 40.89ms | 0% |
| In-memory | 200 | 20ms | 46.45ms | 47.6ms | 53.47ms | 0% |
| In-memory | 400 | 32.79ms | 65.05ms | 67.37ms | 76.23ms | 0% |
| In-memory | 800 | 44.84ms | 79.65ms | 84.73ms | 94.47ms | 0% |
| In-memory | 1600 | 59.73ms | 94.25ms | 120.6ms | 128.97ms | 0% |
| In-memory | 3200 | 299.1ms | 880.01ms | 1.03s | 1.05s | 0% |
| In-memory | 6400 | 450.01ms | 1.09s | 1.14s | 2.92s | 0% |
| In-memory | 12800 | 798.58ms | 2.42s | 3.13s | 4.33s | 0% |
| In-memory | 13000 | 602.45ms | 2.43s | 5.11s | 5.44s | 0.05% |