Skip to content

NeoDFisher/URL-Shortener-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔗 URL Shortener API

A REST API for URL shortening built in PHP 8.2, demonstrating clean architecture with a strict separation between Domain, Application, Infrastructure, and HTTP layers. No full framework — just Slim-style routing, PSR-4 autoloading, and PDO.


📐 Architecture

┌─────────────────────────────────┐
│           HTTP Layer            │  ← Routes, handlers, JSON responses
│  Http/Handler + Http/Response   │
└────────────────┬────────────────┘
                 │
┌────────────────▼────────────────┐
│        Application Layer        │  ← Use cases + DTOs
│   UseCase/  +  DTO/             │
└────────────────┬────────────────┘
                 │
┌────────────────▼────────────────┐
│          Domain Layer           │  ← Entities, interfaces, exceptions
│  Model/  Repository/  Exception/│
└────────────────┬────────────────┘
                 │
┌────────────────▼────────────────┐
│      Infrastructure Layer       │  ← PostgreSQL adapter, DB connection
│  Persistence/  Database/        │
└─────────────────────────────────┘
src/
├── Domain/
│   ├── Model/ShortUrl.php                      # Core entity (immutable, readonly)
│   ├── Repository/ShortUrlRepositoryInterface.php
│   └── Exception/                              # Domain-specific exceptions
├── Application/
│   ├── UseCase/                                # One class per use case
│   │   ├── CreateShortUrl.php
│   │   ├── ResolveShortUrl.php
│   │   ├── ListShortUrls.php
│   │   └── DeleteShortUrl.php
│   └── DTO/                                    # Typed input/output objects
├── Infrastructure/
│   ├── Database/Connection.php                 # PDO connection manager
│   └── Persistence/PostgresShortUrlRepository.php
└── Http/
    ├── Handler/ShortUrlHandler.php             # Request → UseCase → Response
    └── Response/JsonResponse.php              # Consistent JSON envelope

✨ API Endpoints

Method Endpoint Description
POST /urls Create a new short URL
GET /urls List all short URLs
GET /urls/{slug} Resolve a slug (returns JSON)
DELETE /urls/{id} Delete a short URL by ID
GET /{slug} Browser redirect to the original URL

🚀 Installation

Requirements: PHP 8.2+, Composer, PostgreSQL

# Clone and install dependencies
git clone https://github.com/YOUR_USERNAME/url-shortener-api.git
cd url-shortener-api
composer install

# Configure environment
cp .env.example .env
# Edit .env with your PostgreSQL credentials

# Run the database migration
psql -U postgres -d url_shortener -f config/migrations.sql

# Start the development server
php -S localhost:8000 -t public

📖 Usage

Create a short URL (auto-generated slug):

curl -X POST http://localhost:8000/urls \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.example.com/very/long/path"}'
{
  "success": true,
  "data": {
    "id": "a1b2c3d4-...",
    "original_url": "https://www.example.com/very/long/path",
    "slug": "xK3mP2q",
    "short_url": "http://localhost:8000/xK3mP2q",
    "click_count": 0,
    "created_at": "2024-11-01T10:00:00+00:00"
  }
}

Create with a custom slug:

curl -X POST http://localhost:8000/urls \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.example.com", "slug": "my-link"}'

List all URLs:

curl http://localhost:8000/urls

Resolve a slug (API):

curl http://localhost:8000/urls/my-link

Browser redirect — just visit http://localhost:8000/my-link in a browser.

Delete a URL:

curl -X DELETE http://localhost:8000/urls/a1b2c3d4-...

🧪 Running Tests

composer install
./vendor/bin/phpunit

Unit tests use an in-memory fake repository — no database connection needed. This is only possible because the application layer depends on ShortUrlRepositoryInterface, not the concrete PostgreSQL class.

tests/
├── Unit/
│   ├── ShortUrlTest.php          # Domain model invariants (7 tests)
│   ├── CreateShortUrlTest.php    # CreateShortUrl use case (5 tests)
│   └── ResolveAndDeleteTest.php  # Resolve + Delete use cases (5 tests)
└── Integration/                  # (requires live DB — add as needed)

🏗 Design Decisions

Why readonly properties on the domain entity?

ShortUrl uses PHP 8.2 readonly properties. Once constructed, no code can accidentally mutate the entity's fields — the only way to "change" an entity is to create a new one (see withIncrementedClickCount()). This eliminates a whole class of state-related bugs.

Why separate create() and reconstitute() factory methods?

create() enforces all business invariants (valid URL, valid slug). reconstitute() bypasses them — data loaded from the database was already valid when it was saved. Mixing the two would mean either running redundant validation on every DB read, or weakening validation on new entities.

Why DTOs between layers?

ShortUrlResponse decouples the domain model from the API contract. If ShortUrl gains a new internal field, the API response shape stays unchanged until you explicitly add it to the DTO. It also makes it obvious what data crosses each layer boundary.

Why no framework?

The project deliberately avoids pulling in a full framework to show that clean architecture is a design choice, not a framework feature. The routing in public/index.php is 30 lines. Adding Slim or Laravel later requires only wiring the handlers differently — no application code changes.

Why store amounts as TEXT in PostgreSQL?

URLs are stored as TEXT (not VARCHAR(255)) because modern URLs — especially with query strings — easily exceed 255 characters. PostgreSQL's TEXT type has no meaningful performance difference from VARCHAR without a length constraint.


📄 License

MIT

About

A REST API built in PHP that shortens URLs, tracks click counts, and handles browser redirects.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages