Skip to content

API Package

The API package provides a complete REST API with secure authentication, rate limiting, webhooks, and OpenAPI documentation.

Installation

bash
composer require host-uk/core-api

Quick Start

php
<?php

namespace Mod\Blog;

use Core\Events\ApiRoutesRegistering;

class Boot
{
    public static array $listens = [
        ApiRoutesRegistering::class => 'onApiRoutes',
    ];

    public function onApiRoutes(ApiRoutesRegistering $event): void
    {
        $event->routes(function () {
            Route::get('/posts', [Api\PostController::class, 'index']);
            Route::post('/posts', [Api\PostController::class, 'store']);
            Route::get('/posts/{id}', [Api\PostController::class, 'show']);
        });
    }
}

Key Features

Authentication & Security

  • API Keys - Secure API key management with bcrypt hashing
  • Scopes - Fine-grained permission system
  • Rate Limiting - Tier-based rate limits with Redis backend
  • Key Rotation - Secure key rotation with grace periods

Webhooks

Documentation

Monitoring

Authentication

Creating API Keys

php
use Mod\Api\Models\ApiKey;

$apiKey = ApiKey::create([
    'name' => 'Mobile App',
    'workspace_id' => $workspace->id,
    'scopes' => ['posts:read', 'posts:write'],
    'rate_limit_tier' => 'pro',
]);

// Get plaintext key (only shown once!)
$plaintext = $apiKey->plaintext_key; // sk_live_abc123...

Using API Keys

bash
curl -H "Authorization: Bearer sk_live_abc123..." \
     https://api.example.com/v1/posts

Learn more about Authentication →

Rate Limiting

Tier-based rate limits with automatic enforcement:

php
// config/api.php
'rate_limits' => [
    'free' => ['requests' => 1000, 'per' => 'hour'],
    'pro' => ['requests' => 10000, 'per' => 'hour'],
    'business' => ['requests' => 50000, 'per' => 'hour'],
    'enterprise' => ['requests' => null], // Unlimited
],

Rate limit headers included in every response:

X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9847
X-RateLimit-Reset: 1640995200

Learn more about Rate Limiting →

Webhooks

Creating Webhooks

php
use Mod\Api\Models\WebhookEndpoint;

$webhook = WebhookEndpoint::create([
    'url' => 'https://your-app.com/webhooks',
    'events' => ['post.created', 'post.updated'],
    'secret' => 'whsec_abc123...',
    'workspace_id' => $workspace->id,
]);

Dispatching Events

php
use Mod\Api\Services\WebhookService;

$service = app(WebhookService::class);

$service->dispatch('post.created', [
    'id' => $post->id,
    'title' => $post->title,
    'url' => route('posts.show', $post),
]);

Verifying Signatures

php
use Mod\Api\Services\WebhookSignature;

$signature = WebhookSignature::verify(
    payload: $request->getContent(),
    signature: $request->header('X-Webhook-Signature'),
    secret: $webhook->secret
);

if (!$signature) {
    abort(401, 'Invalid signature');
}

Learn more about Webhooks →

OpenAPI Documentation

Auto-generate OpenAPI documentation with attributes:

php
use Mod\Api\Documentation\Attributes\ApiTag;
use Mod\Api\Documentation\Attributes\ApiParameter;
use Mod\Api\Documentation\Attributes\ApiResponse;

#[ApiTag('Posts')]
class PostController extends Controller
{
    #[ApiParameter(name: 'page', in: 'query', type: 'integer')]
    #[ApiParameter(name: 'per_page', in: 'query', type: 'integer')]
    #[ApiResponse(status: 200, description: 'List of posts')]
    public function index(Request $request)
    {
        return PostResource::collection(
            Post::paginate($request->input('per_page', 15))
        );
    }
}

View documentation at:

  • /api/docs - Swagger UI
  • /api/docs/scalar - Scalar interface
  • /api/docs/redoc - ReDoc interface

Learn more about Documentation →

API Resources

Transform models to JSON:

php
<?php

namespace Mod\Blog\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'slug' => $this->slug,
            'excerpt' => $this->excerpt,
            'content' => $this->when(
                $request->user()->tokenCan('posts:read-content'),
                $this->content
            ),
            'status' => $this->status,
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at' => $this->created_at->toIso8601String(),
            'updated_at' => $this->updated_at->toIso8601String(),
        ];
    }
}

Configuration

php
// config/api.php
return [
    'prefix' => 'api/v1',
    'middleware' => ['api'],

    'rate_limits' => [
        'free' => ['requests' => 1000, 'per' => 'hour'],
        'pro' => ['requests' => 10000, 'per' => 'hour'],
        'business' => ['requests' => 50000, 'per' => 'hour'],
        'enterprise' => ['requests' => null],
    ],

    'api_keys' => [
        'hash_algo' => 'bcrypt',
        'prefix' => 'sk',
        'length' => 32,
    ],

    'webhooks' => [
        'max_retries' => 3,
        'retry_delay' => 60, // seconds
        'signature_algo' => 'sha256',
    ],

    'documentation' => [
        'enabled' => true,
        'middleware' => ['web', 'auth'],
        'title' => 'API Documentation',
    ],
];

Best Practices

1. Use API Resources

php
// ✅ Good - API resource
return PostResource::collection($posts);

// ❌ Bad - raw model data
return $posts->toArray();

2. Implement Scopes

php
// ✅ Good - scope protection
Route::middleware('scope:posts:write')
    ->post('/posts', [PostController::class, 'store']);

3. Verify Webhook Signatures

php
// ✅ Good - verify signature
if (!WebhookSignature::verify($payload, $signature, $secret)) {
    abort(401);
}

4. Use Rate Limit Middleware

php
// ✅ Good - rate limited
Route::middleware('api.rate-limit')
    ->group(function () {
        // API routes
    });

Testing

php
<?php

namespace Tests\Feature\Api;

use Tests\TestCase;
use Mod\Api\Models\ApiKey;

class PostApiTest extends TestCase
{
    public function test_lists_posts(): void
    {
        $apiKey = ApiKey::factory()->create([
            'scopes' => ['posts:read'],
        ]);

        $response = $this->withHeaders([
            'Authorization' => "Bearer {$apiKey->plaintext_key}",
        ])->getJson('/api/v1/posts');

        $response->assertOk()
            ->assertJsonStructure([
                'data' => [
                    '*' => ['id', 'title', 'slug'],
                ],
            ]);
    }
}

Learn More

Released under the EUPL-1.2 License.