Skip to content

Repository files navigation

Shortenkai

A URL shortener REST API built with ASP.NET Core 10, PostgreSQL, and Redis.

Tech Stack

  • .NET 10 / ASP.NET Core
  • Entity Framework Core with PostgreSQL (Npgsql)
  • Redis (StackExchange.Redis) for response caching
  • Swagger UI (Swashbuckle)
  • Docker Compose for local orchestration (API + PostgreSQL + Redis)

API Endpoints

All routes are versioned via a URL segment (current version: 1.0). Every response is wrapped in a result envelope: { "isSuccess": bool, "value": ..., "errorMessage": string | null, "isNotFound": bool }.

Method Route Description
POST /api/v1.0 Shorten a URL
GET /api/v1.0/all List all shortened URLs
GET /api/v1.0/get/{code} Get URL metadata by short code
GET /api/v1.0/{code} Redirect to the original URL (by short code or slug)
DELETE /api/v1.0/{code} Delete a single shortened URL by short code
DELETE /api/v1.0 Delete all shortened URLs

POST /api/v1.0

Request body:

{
  "url": "https://example.com/some/very/long/url",
  "slug": "optional-custom-slug",
  "days": 30
}

days controls the Redis cache TTL for the entry and defaults to 30 if omitted.

Response 201 Created:

{
  "isSuccess": true,
  "value": {
    "shortCode": "abc123def4",
    "url": "https://example.com/some/very/long/url",
    "slug": "optional-custom-slug"
  },
  "errorMessage": null,
  "isNotFound": false
}

GET /api/v1.0/all

Returns the metadata (shortCode, url, slug) for every shortened URL.

GET /api/v1.0/get/{code}

Returns the metadata (shortCode, url, slug) for the given short code.

GET /api/v1.0/{code}

Performs an HTTP redirect to the original URL associated with the given short code or slug. The lookup is cached in Redis (TTL set at creation time via days, keyed by slug if provided, otherwise short code) to avoid hitting PostgreSQL on every redirect.

DELETE /api/v1.0/{code}

Deletes the shortened URL matching the given short code from PostgreSQL and evicts its Redis cache entry. Runs inside a DB transaction.

DELETE /api/v1.0

Deletes every shortened URL from PostgreSQL and evicts all corresponding Redis cache entries. Runs inside a DB transaction.

How It Works

The original URL is SHA-256 hashed into a 64-character hex digest, which is then sliced into two distinct fields:

Field Slice Purpose
ShortCode hashHex[0..10] — first 10 hex chars The actual lookup key. It's what GetByCode and GetUrlByCode match against, and what goes in the short URL itself (e.g. /api/v1.0/abc123def4).
KeyCode hashHex[9..] — remaining ~55 hex chars A longer reference to the same hash, returned to the caller as a canonical/display identifier. It is not used for lookups anywhere — metadata only.

Both slices come from the same digest and overlap by one character (index 9 is the last character of ShortCode and also the first character of KeyCode) — that's expected, not a bug, since ShortCode only needs to be short and unique enough to serve as a cache/DB key, while KeyCode exists purely as a longer reference for clients.

On creation, the original URL is also written to Redis under the slug (or ShortCode, if no slug was provided) so that subsequent redirects are served from cache instead of hitting PostgreSQL.

Project Structure

The solution follows Clean Architecture, split into four projects with dependencies pointing inward (APIInfrastructure/ApplicationDomain):

Shortenkai/
├── Shortenkai.Domain/                  # No dependencies — core entities
│   └── Entities/
│       └── UrlEntity.cs                # EF Core entity
├── Shortenkai.Application/             # Depends on Domain — use-case contracts
│   ├── Common/
│   │   └── FAResult.cs                 # Typed result wrapper (Success/NotFound/Failure)
│   ├── DTOs/
│   │   ├── ShortnedUrlRequestDto.cs    # POST request shape
│   │   └── ShortenedUrlResponseDto.cs  # API response shape
│   └── ServicesInterfaces/
│       └── IShortenkaiService.cs
├── Shortenkai.Infrastructure/          # Depends on Domain + Application — implementation details
│   ├── Database/
│   │   └── ShortenkaiUrlDb.cs          # DbContext
│   └── Services/
│       ├── ShortenkaiService.cs        # Business logic & hashing
│       └── CacheService.cs             # Generic Redis cache wrapper
└── Shortenkai.API/                     # Depends on Application + Infrastructure — HTTP host
    ├── Controllers/
    │   └── UrlController.cs
    ├── Properties/
    │   └── launchSettings.json
    ├── appsettings.json
    └── Program.cs                       # App bootstrap & DI setup

Running Locally

Prerequisites

  • .NET 10 SDK
  • PostgreSQL instance
  • Redis instance (defaults to localhost:6379, configured in Program.cs)

Setup

  1. Clone the repository
  2. Set the database connection string via .NET User Secrets instead of editing appsettings.json directly — this keeps the password out of source control:
cd Shortenkai.API
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;Port=5433;Database=Shortenkai;Username=postgres;Password=<your-password>"
  1. Start the app:
dotnet run --project Shortenkai.API

Swagger UI is served at the root path (/) in development mode.

Running with Docker

The full stack (API + PostgreSQL + Redis) is defined in docker-compose.yml, which builds the API from the Dockerfile (multi-stage: SDK image builds/publishes, then runs on the SDK runtime).

Prerequisites

  • Docker and Docker Compose

Setup

  1. Create a .env file in the project root (gitignored) with the variables consumed by docker-compose.yml:
DB_USER=postgres
DB_PASSWORD=<your-password>
DB_NAME=Shortenkai
DB_DATA_PATH=./postgres_data
REDIS_PORT=6379
POSTGRESQL_PORT=5433
API_PORT=5279
  1. Start the stack:
docker compose up --build

This spins up three containers:

Service Container Description
web shortenkai_api The API, built from the Dockerfile, listening on ${API_PORT} (mapped to container port 8080)
db shortenkai_db PostgreSQL, seeded on first run via sql/db_init.sql, with a healthcheck gating API startup
cache shortenkai_cache Redis (redis-stack-server), exposed on ${REDIS_PORT}

The API waits for db to report healthy before starting. Postgres data persists on the host at ${DB_DATA_PATH}.

Once running, Swagger UI is available at http://localhost:${API_PORT}/.

  1. Stop the stack:
docker compose down

Inspecting the Redis Cache

If Redis is running in a Docker container, open a redis-cli session inside it:

docker exec -it <container_name_or_id> redis-cli

Not sure of the name? Run docker ps and look for the Redis image.

Listing cached keys

All keys are prefixed with the configured InstanceName (Shortenkai:):

KEYS Shortenkai:*

Reading a cached value

IDistributedCache (what CacheService wraps) does not store entries as plain Redis strings — Microsoft.Extensions.Caching.StackExchangeRedis stores each entry as a hash, with fields data (the serialized value) and absexp (absolute expiration, as .NET ticks). A plain GET on the key returns nothing useful; use HGETALL instead:

HGETALL "Shortenkai:abc123def4"

Example output:

1) "absexp"
2) "638508096000000000"
3) "data"
4) "\"https://example.com/some/very/long/url\""

data holds the JSON produced by CacheService.SetAsync — since the cached value here is a string, it's serialized as a JSON string (hence the surrounding escaped quotes).

About

A URL shortener REST API built with ASP.NET Core 10, PostgreSQL, and Redis.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages