A high-performance URL shortener API built with modern technologies including Elysia, Bun runtime, PostgreSQL, Redis, and Better Auth.
- Runtime: Bun v1.2.22
- Framework: Elysia - Fast and ergonomic web framework
- Database: PostgreSQL 17.2 - User management, authentication, and URL storage
- Cache: Redis 7.4 - Caching and analytics (using Bun's native Redis client)
- ORM: Drizzle ORM - TypeScript ORM for PostgreSQL
- Authentication: Better Auth
- Validation: Zod
- API Documentation: OpenAPI/Swagger
- Code Quality: Biome (linting & formatting)
- CI/CD: GitHub Actions with Semantic Release
- β User authentication with Better Auth
- β PostgreSQL database with Drizzle ORM
- β Redis caching support with cache invalidation
- β UUID primary keys with PostgreSQL gen_random_uuid()
- β OpenAPI/Swagger documentation
- β CORS configuration
- β Docker & Docker Compose support with multi-arch builds
- β Automated CI/CD pipeline
- β Semantic versioning and releases
- β URL shortening with public/private access control
- β High-performance URL storage in PostgreSQL
- β Like/Unlike public URLs with duplicate prevention
- β Fetch public URLs with filtering, sorting, and pagination
- β URL access tracking with Redis
- β Dual ranking system (most viewed & most liked URLs)
- β Real-time analytics with Redis-based view counter
- β PostgreSQL for scalable URL read operations
- β Comprehensive test coverage (97+ unit tests)
- β E2E testing with schema isolation
- β Type-safe error handling
- β Domain-driven design architecture
src/
βββ core/
β βββ entities/ # Base entities and value objects
β βββ errors/ # Custom error classes
β βββ repositories/ # Base repository interfaces
βββ domain/
β βββ url-shortening/
β βββ application/
β β βββ repositories/ # Repository interfaces
β β βββ use-cases/ # Business use cases (97+ tests)
β β βββ url-code/ # URL code generation interface
β βββ enterprise/
β βββ entities/ # Domain entities (Url, User)
β βββ value-objects/ # Value objects (UrlWithAuthor)
βββ system/
β βββ application/
β βββ repositories/ # System repository interfaces
β β βββ system-health-repository.ts # Health check interface
β βββ use-cases/ # System use cases
β βββ check-services-health.ts # Health check logic
βββ infra/
β βββ cache/ # Cache layer abstraction
β β βββ cache-repository.ts # Cache interface for Cache-Aside pattern
β βββ http/
β β βββ controllers/ # HTTP controllers
β β β βββ auth/ # Authenticated routes
β β β β βββ create-url-controller.ts # Create shortened URLs
β β β β βββ update-url-controller.ts # Update URLs
β β β β βββ delete-url-controller.ts # Delete URLs
β β β β βββ get-url-by-id-controller.ts # Get URL by ID
β β β β βββ like-url-controller.ts # Like a public URL
β β β β βββ unlike-url-controller.ts # Unlike a URL
β β β β βββ fetch-user-liked-urls-controller.ts # Get user's liked URLs
β β β β βββ fetch-user-urls-controller.ts # Get user's own URLs
β β β βββ public/ # Public routes
β β β βββ health-controller.ts # Health checks
β β β βββ fetch-many-public-urls-controller.ts # Browse public URLs
β β β βββ get-ranking-controller.ts # Top 10 URLs by access
β β β βββ get-url-by-code-controller.ts # Redirect short URLs
β β βββ presenters/ # HTTP response presenters
β β β βββ url-presenter.ts # URL entity presenter
β β β βββ url-with-author-presenter.ts # URL with author presenter
β β β βββ pagination-presenter.ts # Pagination response presenter
β β βββ plugins/ # Elysia plugins
β β β βββ better-auth.ts # Better Auth plugin
β β β βββ openapi.ts # OpenAPI/Swagger plugin
β β βββ utils/ # HTTP utilities
β β βββ schemas/ # Zod validation schemas
β βββ jwt/
β β βββ jwt-config.ts # JWT configuration
β β βββ jwt-auth-plugin.ts # JWT authentication plugin for API key validation
β βββ system/
β β βββ repositories/ # System infrastructure implementations
β β βββ system-health-repository.ts # Redis & PostgreSQL health checks
β βββ db/
β β βββ drizzle/ # PostgreSQL implementation
β β β βββ repositories/ # Drizzle repositories
β β β β βββ drizzle-urls-repository.ts # URLs with Cache-Aside
β β β β βββ drizzle-users-repository.ts # User management
β β β βββ mappers/ # Domain <-> Drizzle mappers
β β β β βββ drizzle-url-mapper.ts
β β β β βββ drizzle-url-with-author-mapper.ts
β β β β βββ drizzle-user-mapper.ts
β β β βββ schema/ # Database schema (UUID v4)
β β β βββ client.ts # Database connection
β β βββ redis/ # Redis implementation
β β βββ repositories/ # Redis repositories
β β β βββ redis-analysis-repository.ts # Analytics & ranking
β β β βββ redis-cache-repository.ts # Cache-Aside implementation
β β β βββ redis-secondary-storage-repository.ts # Better Auth session caching
β β βββ client.ts # Redis connection
β βββ storage/ # Storage layer abstraction
β β βββ secondary-storage-repository.ts # Secondary storage interface
β βββ factories/ # Dependency injection factories (13 factories)
β βββ url-code/ # URL code generator implementation
β β βββ hash-url-code-generator.ts # Hashids with base64 URL-safe
β βββ lib/
β β βββ auth.ts # Better Auth configuration
β β βββ hashids.ts # Hashids configuration
β βββ env.ts # Environment variables schema
βββ test/
β βββ e2e/ # E2E test helpers
β β βββ auth-helpers.ts # Better Auth test utilities
β βββ repositories/ # In-memory repository implementations
β β βββ in-memory-urls-repository.ts
β β βββ in-memory-users-repository.ts
β β βββ in-memory-analysis-repository.ts
β βββ factories/ # Test data factories
β βββ url-code/ # URL code generator for tests
β βββ fake-hash-url-code-generator.ts # Base62 for testing
βββ index.ts # Application entry point
- Bun >= 1.2.22
- Docker & Docker Compose (recommended)
- PostgreSQL 17+ (or use Docker)
- Redis 7+ (or use Docker)
- Clone the repository:
git clone <repository-url>
cd url-shortener-api- Install dependencies:
bun install- Copy environment variables:
cp .env.example .env- Configure your `.env` file with the required values:
NODE_ENV=development
PORT=3333
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
DATABASE_USERNAME=your_database_username
DATABASE_PASSWORD=your_database_password
DATABASE_NAME=your_database_name
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
REDIS_PASSWORD=your_redis_password
REDIS_CODE_ID=1000
SECRET_HASH_KEY=your_secret_hash_key_for_url_encoding
CLIENT_URL=http://localhost:3000
BETTER_AUTH_SECRET=your_secret_key
BETTER_AUTH_URL=http://localhost:3333- Start all services (PostgreSQL & Redis):
bun run docker:up- Generate and run PostgreSQL migrations:
bun run db:generate
bun run db:migrate- Start the development server:
bun run dev- Open http://localhost:3333 in your browser.
Ensure PostgreSQL and Redis are running locally, then:
bun run db:migrate
bun run dev- `bun run dev` - Start development server with hot reload
- `bun run db:studio` - Open Drizzle Studio (database GUI)
- `bun run db:generate` - Generate migration files from schema
- `bun run db:migrate` - Run pending migrations
- `bun run db:push` - Push schema changes directly (dev only)
- `bun run docker:up` - Start PostgreSQL and Redis containers
- `bun run docker:down` - Stop and remove containers
- `bun run docker:logs` - View container logs
- `bun run docker:restart` - Restart containers
- `bun run docker:build` - Build application Docker image
- `bun run docker:build:prod` - Build optimized production image
- `bun run docker:run` - Run application in Docker
- `bun run docker:tag:hub` - Tag images for Docker Hub (viniciusaf/url-shortener-api)
- `bun run docker:push` - Push images to Docker Hub
- `bun run docker:build:push` - Build production image, tag, and push to Docker Hub
- `bun run test` - Run unit tests (src/core and src/domain)
- `bun run test:unit` - Run unit tests (alias for test)
- `bun run test:unit:watch` - Run unit tests in watch mode
- `bun run test:e2e` - Run E2E tests with schema isolation
- `bun run lint` - Lint code with Biome
- `bun run lint:fix` - Fix linting issues
- `bun run format` - Check code formatting
- `bun run format:fix` - Format code
- `bun run check` - Run all checks (lint + format)
- `bun run check:fix` - Fix all issues
The application uses PostgreSQL as its primary database, handling:
User Management & Authentication:
- users - User accounts with email/password authentication
- sessions - Active user sessions
- accounts - OAuth provider accounts
- verifications - Email verification tokens
URL Management:
- urls - Shortened URLs with metadata, including:
- URL code for short links
- Original destination URL
- Public/private access control
- Like counts and analytics
- Author relationships
All tables use UUID v4 for primary keys via PostgreSQL's gen_random_uuid() function, providing:
- Cryptographically secure random IDs
- Better cross-platform compatibility
- Native PostgreSQL generation without runtime dependencies
- Caching - Reduces database load for frequently accessed data
- Analytics - Real-time URL access tracking and ranking
- Session storage - Better Auth secondary storage with 10-minute cookie cache
- View counters - Atomic increment operations for URL views
- Performance - Leverages Bun's built-in Redis client for optimal performance
The application implements the Cache-Aside (Lazy Loading) pattern for optimal performance:
Read Flow:
- Check cache first
- Cache Hit: Deserialize data and reconstruct domain entities
- Cache Miss: Fetch from PostgreSQL β Store in Redis β Return data
Write Flow:
- Update PostgreSQL database
- Invalidate affected cache keys
- Next read will refresh the cache (lazy loading)
Implementation Details:
- Cache Key:
urls-most-liked- Stores top URLs by like count - Data Format: Drizzle raw format (database representation)
- Deserialization:
DrizzleUrlWithAuthorMapper.fromCache()reconstructs domain entities - TTL: 15 minutes for cached data
- Invalidation: Automatic on URL update/delete operations
Cache Invalidation Triggers:
- URL updated (
save()method) - URL deleted (
delete()method) - Ensures data consistency between cache and database
This pattern provides:
- β Reduced database load for frequently accessed rankings
- β Fast response times for popular queries
- β Automatic cache refresh on data changes
- β Domain entity integrity (proper deserialization)
The application implements a dual-layer session caching strategy using Better Auth:
Cookie Cache Layer (First Layer):
- 10-minute TTL for session cookies
- Reduces round-trips to Redis and PostgreSQL
- Fastest session validation for active users
Redis Secondary Storage (Second Layer):
- Implemented via
RedisSecondaryStorageRepository - Stores session data with optional TTL
- Provides distributed session lookups across multiple instances
- Falls back to PostgreSQL when cache misses occur
PostgreSQL (Persistent Layer):
- Authoritative source for all session data
- Handles session creation, updates, and deletions
- Ensures data consistency across distributed caches
Session Lookup Flow:
- Check cookie cache (10-minute TTL)
- Cache Hit: Return session immediately
- Cache Miss: Check Redis secondary storage
- Redis Hit: Return cached session, update cookie cache
- Redis Miss: Query PostgreSQL, populate both caches
This architecture provides:
- β Sub-millisecond session validation for active users
- β Horizontal scalability with Redis-backed sessions
- β Reduced database load for authentication requests
- β Graceful degradation if Redis is unavailable
Authentication is handled by Better Auth with:
- Email/password authentication with Bun's native password hashing
- Session management with dual-layer caching:
- Cookie cache (10 minutes TTL)
- Redis secondary storage for distributed session lookups
- PostgreSQL adapter via Drizzle ORM for persistent session storage
- Auto sign-in after registration
The `docker-compose.yaml` includes:
- PostgreSQL 17.2 - User management, authentication, and URL storage
- Redis 7.4 - Caching layer and URL access tracking
# Build with latest tag and git SHA
bun run docker:build:prod
# Run the container
bun run docker:runThe project includes scripts to build, tag, and push Docker images to Docker Hub under the `viniciusaf` username:
# Tag images for Docker Hub
bun run docker:tag:hub
# Push images to Docker Hub (requires authentication)
bun run docker:push
# One-command build, tag, and push
bun run docker:build:pushRequirements:
- Docker Hub account (username: `viniciusaf`)
- Logged in to Docker: `docker login`
- Images are tagged with:
- `latest` - Current production build
- Git short SHA - Specific commit version
The project includes a GitHub Actions workflow (.github/workflows/ci.yaml) that automatically:
- β Runs code quality checks (Biome linting & formatting)
- β Runs unit tests (Bun test runner)
- β Runs E2E tests with PostgreSQL and Redis services
- β Builds the application
- β Builds production Docker image with multi-stage build
- β
Pushes Docker images to Docker Hub with tags:
latest- Current production build- Git commit SHA - Specific version
- β Handles semantic versioning and releases
- π§ AWS deployment (configured, not enabled)
Images are automatically published to Docker Hub under:
- Registry:
viniciusaf/url-shortener-api - Authentication: Uses GitHub secret
DOCKER_HUB_TOKEN(personal access token) - Variables Used:
DOCKER_HUB_USERNAME- GitHub repository variableSERVICE_NAME- GitHub repository variableDOCKER_HUB_TOKEN- GitHub secret (personal access token)
API documentation is available via OpenAPI/Scalar at:
- Development: http://localhost:3333/api/openapi
- GET
/api/public/:code- Redirect to destination URL- Returns 302 redirect to the original URL
- Increments access count for ranking
- Works with both public and private URLs
- GET
/api/public/urls- Browse public URLs (Requires API Key)- Query Parameters:
page(number, default: 1) - Page numberper_page(number, default: 10) - Items per pagesearch(string, optional) - Search by name or keywordsorder(enum, optional) - Sort order:created_at,updated_at,-created_at,-updated_atcreated_at_gte(date, optional) - Filter by creation date (yyyy-mm-dd)updated_at_gte(date, optional) - Filter by update date (yyyy-mm-dd)
- Returns paginated list of public URLs with author information
- Query Parameters:
- GET
/api/public/ranking- Get top 10 most accessed URLs- Returns URLs ranked by access count (most viewed first)
- Only includes public URLs
- No authentication required
-
POST
/api/urls- Create a shortened URL- Body:
{ name, destination_url, description?, is_public } - Returns the created URL with generated short code
- Body:
-
GET
/api/urls/:id- Get a URL by ID- Returns URL details by its UUID
- Returns 404 if not found
-
PUT
/api/urls/:id- Update a URL- Requires ownership verification
- Body:
{ name, destination_url, description?, is_public } - Returns the updated URL
- Returns 405 if the authenticated user is not the owner
-
DELETE
/api/urls/:id- Delete a URL- Requires ownership verification
- Returns 204 on success
-
PATCH
/api/urls/:id/like- Like a public URL- Returns 400 if already liked
- Returns 405 if the URL is private
-
PATCH
/api/urls/:id/unlike- Unlike a URL- Succeeds even if the URL was not previously liked
-
GET
/api/urls/me- Get authenticated user's URLs- Query Parameters:
page(number, default: 1) - Page numberper_page(number, default: 10) - Items per pagesearch(string, optional) - Search by name or keywordsis_public(boolean, optional) - Filter by public/privateorder(enum, optional) - Sort ordercreated_at_gte(date, optional) - Filter by creation dateupdated_at_gte(date, optional) - Filter by update date
- Returns paginated list of user's own URLs
- Query Parameters:
-
GET
/api/urls/liked- Get user's liked URLs- Returns array of URLs that the user has liked
- Only includes public URLs
The application provides two health check endpoints following Kubernetes best practices:
Basic health check to verify the application is running:
curl http://localhost:3333/healthzResponse (200 OK):
{
"message": "Ok"
}Comprehensive health check for external service dependencies:
curl http://localhost:3333/readyzResponse (200 OK) - All services healthy:
{
"status": "ok",
"services": {
"redis": true,
"db": true
}
}Response (503 Service Unavailable) - Service degraded:
{
"status": "down",
"services": {
"redis": false,
"db": true
}
}Checked Services:
- PostgreSQL - Database connection via
SELECT 1query - Redis - Cache connection via
PINGcommand
Architecture:
- Uses Clean Architecture with
CheckServicesHealthUseCase - Repository pattern with
SystemHealthRepositoryinterface - Implementation in
InfraSystemHealthRepository - Factory pattern for dependency injection via
makeCheckServicesHealthUseCase()
The application implements domain-driven design with comprehensive use cases for URL management:
- CreateUrlUseCase - Create shortened URLs with unique codes
- UpdateUrlUseCase - Update URL properties (name, value, description, visibility)
- DeleteUrlUseCase - Delete URLs with ownership verification
- GetUrlByIdUseCase - Retrieve URL by ID
- GetUrlByCodeUseCase - Retrieve URL by shortening code (for redirects)
- FetchManyPublicUrlsUseCase - Browse public URLs with:
- Pagination (page, perPage)
- Search by name
- Sorting (by created_at, updated_at)
- Date filtering (createdAtGte, updatedAtGte)
- Built-in caching with cache invalidation
- LikeUrlUseCase - Like public URLs (prevents:
- Liking private URLs (NotAllowedError)
- Duplicate likes (UrlAlreadyLikedError)
- Non-existent URLs (ResourceNotFoundError)
- UnlikeUrlUseCase - Unlike URLs with automatic count management
- GetRankingUseCase - Get top 10 most accessed URLs
- Tracks URL access count via Redis cache
- Returns URLs sorted by view count (descending)
- Automatically increments on each URL access via
GetUrlByCodeUseCase - Uses Redis ZREVRANGE format for efficient ranking
- GetRankingByMostLikedUseCase - Get top most liked URLs
- Returns public URLs sorted by like count (descending)
- Configurable limit (default: 10)
- Only includes public URLs in ranking
- Real-time data from database
- β Authorization checks (verify user ownership)
- β Type-safe error handling with Either pattern
- β Pagination with metadata
- β Cache layer with TTL support
- β Atomic operations for likes/unlikes
- β URL access tracking with Redis
- β Dual ranking system (by views and by likes)
- β Comprehensive test coverage (97+ tests)
The application uses the Factory Pattern for dependency injection, located in src/infra/factories/. Each use case has a corresponding factory function that wires up all required dependencies.
All use case factories are available through a barrel export:
import {
makeCreateUrlUseCase,
makeGetUrlByCodeUseCase,
makeLikeUrlUseCase,
// ... and 9 more factories
} from '@/infra/factories';URL Management:
makeCreateUrlUseCase- Create shortened URLsmakeGetUrlByCodeUseCase- Retrieve URLs by codemakeGetUrlByIdUseCase- Retrieve URLs by IDmakeUpdateUrlUseCase- Update URL propertiesmakeDeleteUrlUseCase- Delete URLs
User Interactions:
makeLikeUrlUseCase- Like URLsmakeUnlikeUrlUseCase- Unlike URLs
Data Fetching:
makeFetchUserUrlsUseCase- Fetch user's URLsmakeFetchUserLikedUrlsUseCase- Fetch liked URLsmakeFetchManyPublicUrlsUseCase- Browse public URLs
Analytics:
makeGetRankingUseCase- Get most accessed URLsmakeGetRankingByMostLikedUseCase- Get most liked URLs
System:
makeCheckServicesHealthUseCase- Check Redis and PostgreSQL health
// In your HTTP controller
import { makeCreateUrlUseCase } from '@/infra/factories';
const createUrlUseCase = makeCreateUrlUseCase();
const result = await createUrlUseCase.execute({
authorId: user.id,
name: 'My Link',
destinationUrl: 'https://example.com',
isPublic: true
});
if (result.isRight()) {
const { url } = result.value;
// Handle success
} else {
// Handle error
}Each factory automatically wires up:
- DrizzleUrlsRepository - PostgreSQL URL storage with cache-aside pattern
- DrizzleUsersRepository - PostgreSQL user management
- RedisAnalysisRepository - Redis analytics and URL access tracking
- RedisCacheRepository - Redis cache layer (Cache-Aside pattern implementation)
- RedisSecondaryStorageRepository - Better Auth session caching (integrated in auth.ts)
- HashUrlCodeGenerator - URL code generation using Hashids with base64 URL-safe alphabet
This approach ensures:
- β Clean separation of concerns
- β Easy testing with dependency substitution
- β Centralized dependency configuration
- β Type safety throughout the application
The project uses Bun's native test runner with in-memory repository implementations for fast, isolated unit tests.
Located in src/test/repositories/, these implementations allow testing without a database:
-
InMemoryUrlsRepository - Implements
UrlsRepositoryinterface- Manages URL entities with full CRUD operations
- Supports sorting (by created_at, updated_at, title, description, value, isPublic)
- Implements pagination
- Provides methods to query public URLs and filter by author
findManyByIds- Bulk fetch URLs with author informationfindManyByMostLiked- Get top URLs sorted by like count
-
InMemoryUsersRepository - Implements
UsersRepositoryinterface- Manages user entities
- Supports lookup by email (for authentication)
- Supports lookup by ID
-
InMemoryCacheRepository - Implements
CacheRepositoryinterface- Manages ID counter for URL code generation
- Provides atomic increment operations
- Stores and retrieves cached data with TTL support
- Supports cache expiration and invalidation
incrementBy- Atomic increment for URL view trackinggetUrlRanking- Retrieve top URLs in Redis ZREVRANGE format
# Run unit tests
bun run test
# Run unit tests in watch mode
bun run test:watch
# Run E2E tests
bun run test:e2eTests are located in:
src/core/**/*.test.ts- Core layer testssrc/domain/**/*.test.ts- Domain layer testssrc/infra/http/controllers/**/*.e2e.spec.ts- E2E tests
E2E tests use:
- Schema Isolation - Each test suite gets its own PostgreSQL schema
- Better Auth Integration - Test helpers for authentication (
src/test/e2e/auth-helpers.ts) - Faker.js - Generate realistic test data
- Automatic Cleanup - Schemas are dropped after tests complete
E2E Test Setup:
setup-e2e.ts- Global test setup with schema isolationauth-helpers.ts- Authentication utilities for creating test users- Test files follow
.e2e.spec.tsnaming pattern
Testing Best Practices:
- Unit Tests: Use in-memory repositories for unit tests to avoid database dependencies
- E2E Tests: Test complete request/response flows with real database
- Test use cases and business logic in isolation
- Focus on behavior rather than implementation details
- Mock external dependencies in unit tests
| Variable | Description | Required | Default |
|---|---|---|---|
| `NODE_ENV` | Environment (development/production/test) | Yes | development |
| `PORT` | Server port | Yes | 3333 |
| `DATABASE_URL` | PostgreSQL connection string | Yes | - |
| `DATABASE_USERNAME` | Database username | Yes | - |
| `DATABASE_PASSWORD` | Database password | Yes | - |
| `DATABASE_NAME` | Database name | Yes | - |
| `REDIS_HOST` | Redis server hostname | Yes | localhost |
| `REDIS_PORT` | Redis server port | Yes | 6379 |
| `REDIS_DB` | Redis database number | Yes | 0 |
| `REDIS_PASSWORD` | Redis password | Yes | - |
| `REDIS_CODE_ID` | Starting ID for URL code generation | Yes | - |
| `SECRET_HASH_KEY` | Secret key for Hashids URL encoding | Yes | - |
| `CLIENT_URL` | Frontend URL for CORS | Yes | - |
| `BETTER_AUTH_SECRET` | Secret key for auth tokens | Yes | - |
| `BETTER_AUTH_URL` | Base URL of the API | Yes | - |
This is a portfolio project currently under active development. Contributions, issues, and feature requests are welcome!
This project is part of a portfolio and is available for reference and learning purposes.
Built with β€οΈ using Bun and Elysia