Skip to content

viniciusferreira7/url-shortener-api

Repository files navigation

URL Shortener API

A high-performance URL shortener API built with modern technologies including Elysia, Bun runtime, PostgreSQL, Redis, and Better Auth.

πŸš€ Tech Stack

  • 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

πŸ“‹ Features

Core Features

  • βœ… 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 Management

  • βœ… 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

Analytics & Ranking

  • βœ… Dual ranking system (most viewed & most liked URLs)
  • βœ… Real-time analytics with Redis-based view counter
  • βœ… PostgreSQL for scalable URL read operations

Quality Assurance

  • βœ… Comprehensive test coverage (97+ unit tests)
  • βœ… E2E testing with schema isolation
  • βœ… Type-safe error handling
  • βœ… Domain-driven design architecture

πŸ—οΈ Project Structure

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

πŸ› οΈ Prerequisites

  • Bun >= 1.2.22
  • Docker & Docker Compose (recommended)
  • PostgreSQL 17+ (or use Docker)
  • Redis 7+ (or use Docker)

πŸ“¦ Installation

  1. Clone the repository:
git clone <repository-url>
cd url-shortener-api
  1. Install dependencies:
bun install
  1. Copy environment variables:
cp .env.example .env
  1. 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

πŸš€ Quick Start

Using Docker (Recommended)

  1. Start all services (PostgreSQL & Redis):
bun run docker:up
  1. Generate and run PostgreSQL migrations:
bun run db:generate
bun run db:migrate
  1. Start the development server:
bun run dev
  1. Open http://localhost:3333 in your browser.

Without Docker

Ensure PostgreSQL and Redis are running locally, then:

bun run db:migrate
bun run dev

πŸ“ Available Scripts

Development

  • `bun run dev` - Start development server with hot reload
  • `bun run db:studio` - Open Drizzle Studio (database GUI)

Database

  • `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)

Docker

  • `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

Testing

  • `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

Code Quality

  • `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

πŸ—„οΈ Database Architecture

PostgreSQL (via Drizzle ORM)

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

Redis (Bun Native Client)

  • 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

Cache-Aside Pattern Implementation

The application implements the Cache-Aside (Lazy Loading) pattern for optimal performance:

Read Flow:

  1. Check cache first
  2. Cache Hit: Deserialize data and reconstruct domain entities
  3. Cache Miss: Fetch from PostgreSQL β†’ Store in Redis β†’ Return data

Write Flow:

  1. Update PostgreSQL database
  2. Invalidate affected cache keys
  3. 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)

Session Caching Architecture

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:

  1. Check cookie cache (10-minute TTL)
  2. Cache Hit: Return session immediately
  3. Cache Miss: Check Redis secondary storage
  4. Redis Hit: Return cached session, update cookie cache
  5. 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

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

🐳 Docker Support

Services

The `docker-compose.yaml` includes:

  • PostgreSQL 17.2 - User management, authentication, and URL storage
  • Redis 7.4 - Caching layer and URL access tracking

Building the Application

# Build with latest tag and git SHA
bun run docker:build:prod

# Run the container
bun run docker:run

Publishing to Docker Hub

The 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:push

Requirements:

  • 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

πŸ”„ CI/CD Pipeline

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)

Docker Hub Integration

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 variable
    • SERVICE_NAME - GitHub repository variable
    • DOCKER_HUB_TOKEN - GitHub secret (personal access token)

🌐 API Documentation

API documentation is available via OpenAPI/Scalar at:

πŸ“‘ API Endpoints

Public Endpoints (No Authentication Required)

URL Redirection

  • 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

URL Discovery

  • GET /api/public/urls - Browse public URLs (Requires API Key)
    • Query Parameters:
      • page (number, default: 1) - Page number
      • per_page (number, default: 10) - Items per page
      • search (string, optional) - Search by name or keywords
      • order (enum, optional) - Sort order: created_at, updated_at, -created_at, -updated_at
      • created_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

Analytics

  • 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

Authenticated Endpoints (Requires Session)

URL Management

  • POST /api/urls - Create a shortened URL

    • Body: { name, destination_url, description?, is_public }
    • Returns the created URL with generated short code
  • 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

User's URLs

  • GET /api/urls/me - Get authenticated user's URLs

    • Query Parameters:
      • page (number, default: 1) - Page number
      • per_page (number, default: 10) - Items per page
      • search (string, optional) - Search by name or keywords
      • is_public (boolean, optional) - Filter by public/private
      • order (enum, optional) - Sort order
      • created_at_gte (date, optional) - Filter by creation date
      • updated_at_gte (date, optional) - Filter by update date
    • Returns paginated list of user's own URLs
  • GET /api/urls/liked - Get user's liked URLs

    • Returns array of URLs that the user has liked
    • Only includes public URLs

πŸ₯ Health Checks

The application provides two health check endpoints following Kubernetes best practices:

Liveness Probe (/healthz)

Basic health check to verify the application is running:

curl http://localhost:3333/healthz

Response (200 OK):

{
  "message": "Ok"
}

Readiness Probe (/readyz)

Comprehensive health check for external service dependencies:

curl http://localhost:3333/readyz

Response (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 1 query
  • Redis - Cache connection via PING command

Architecture:

  • Uses Clean Architecture with CheckServicesHealthUseCase
  • Repository pattern with SystemHealthRepository interface
  • Implementation in InfraSystemHealthRepository
  • Factory pattern for dependency injection via makeCheckServicesHealthUseCase()

πŸ“š Use Cases

The application implements domain-driven design with comprehensive use cases for URL management:

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

User Interactions

  • LikeUrlUseCase - Like public URLs (prevents:
    • Liking private URLs (NotAllowedError)
    • Duplicate likes (UrlAlreadyLikedError)
    • Non-existent URLs (ResourceNotFoundError)
  • UnlikeUrlUseCase - Unlike URLs with automatic count management

Analytics & Ranking

  • 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

Key Features

  • βœ… 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)

🏭 Dependency Injection

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.

Factory Functions

All use case factories are available through a barrel export:

import {
  makeCreateUrlUseCase,
  makeGetUrlByCodeUseCase,
  makeLikeUrlUseCase,
  // ... and 9 more factories
} from '@/infra/factories';

Available Factories

URL Management:

  • makeCreateUrlUseCase - Create shortened URLs
  • makeGetUrlByCodeUseCase - Retrieve URLs by code
  • makeGetUrlByIdUseCase - Retrieve URLs by ID
  • makeUpdateUrlUseCase - Update URL properties
  • makeDeleteUrlUseCase - Delete URLs

User Interactions:

  • makeLikeUrlUseCase - Like URLs
  • makeUnlikeUrlUseCase - Unlike URLs

Data Fetching:

  • makeFetchUserUrlsUseCase - Fetch user's URLs
  • makeFetchUserLikedUrlsUseCase - Fetch liked URLs
  • makeFetchManyPublicUrlsUseCase - Browse public URLs

Analytics:

  • makeGetRankingUseCase - Get most accessed URLs
  • makeGetRankingByMostLikedUseCase - Get most liked URLs

System:

  • makeCheckServicesHealthUseCase - Check Redis and PostgreSQL health

Usage Example

// 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
}

Injected Dependencies

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

πŸ§ͺ Testing

Test Infrastructure

The project uses Bun's native test runner with in-memory repository implementations for fast, isolated unit tests.

In-Memory Repositories

Located in src/test/repositories/, these implementations allow testing without a database:

  • InMemoryUrlsRepository - Implements UrlsRepository interface

    • 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 information
    • findManyByMostLiked - Get top URLs sorted by like count
  • InMemoryUsersRepository - Implements UsersRepository interface

    • Manages user entities
    • Supports lookup by email (for authentication)
    • Supports lookup by ID
  • InMemoryCacheRepository - Implements CacheRepository interface

    • 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 tracking
    • getUrlRanking - Retrieve top URLs in Redis ZREVRANGE format

Running Tests

# Run unit tests
bun run test

# Run unit tests in watch mode
bun run test:watch

# Run E2E tests
bun run test:e2e

Tests are located in:

  • src/core/**/*.test.ts - Core layer tests
  • src/domain/**/*.test.ts - Domain layer tests
  • src/infra/http/controllers/**/*.e2e.spec.ts - E2E tests

E2E Testing

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 isolation
  • auth-helpers.ts - Authentication utilities for creating test users
  • Test files follow .e2e.spec.ts naming pattern

Testing Best Practices:

  1. Unit Tests: Use in-memory repositories for unit tests to avoid database dependencies
  2. E2E Tests: Test complete request/response flows with real database
  3. Test use cases and business logic in isolation
  4. Focus on behavior rather than implementation details
  5. Mock external dependencies in unit tests

πŸ“„ Environment Variables

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 -

🀝 Contributing

This is a portfolio project currently under active development. Contributions, issues, and feature requests are welcome!

πŸ“ License

This project is part of a portfolio and is available for reference and learning purposes.

πŸ”— Links


Built with ❀️ using Bun and Elysia

About

A high-performance URL shortener API built with modern technologies including Elysia, Bun runtime, PostgreSQL, Redis, and Better Auth.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages