Skip to main content
Laravel, shipping fast.

Let’s start with something simple: a health check endpoint. It sits there and tells you the API is alive and well. It might sound trivial, but this is where we establish the patterns you’ll use for every endpoint that comes after. Keep it simple, keep it consistent, and you’ll have a blueprint for everything else.

// routes/api.php
Route::get('/health', HealthController::class);

A health check should be pure signal: fast, lightweight, and zero-cost to call. You're just checking that critical services are connected, your database, cache layer, and queue system. No deep queries. No external API calls. No side effects. Just a clean 200 when everything's working, or a 503 when something's broken. Your monitoring systems ping this endpoint every 5-10 seconds and immediately know your API's status without adding load. That's the entire point, nothing more.

This is a public endpoint, no authentication, no middleware (for now). Anyone can call it, no questions asked. Your load balancers ping it. Your monitoring systems ping it. Your deployment pipeline pings it. Everyone's checking the same thing: is your API alive and its services responding?

{
    "data": {
        "status": "ok",
        "timestamp": "2026-02-11T21:50:47+00:00",
        "services": {
            "database": "connected",
            "cache": "connected",
            "queue": "active"
        }
    }
}

The Quick Way

// app/Http/Controllers/HealthController.php
final readonly class HealthController
{
    public function __invoke(): Response
    {
        return response()->json([
            'data' => [
                'status' => 'ok',
                'timestamp' => now()->toIso8601String(),
                'services' => [
                    'database' => 'connected',
                    'cache' => 'connected',
                    'queue' => 'active',
                ],
            ]
        ]);
    }
}

First, think about what we’re doing.

We’re handling an HTTP request. We need to check that critical services are working. We need to return a response. That's three steps.

As your API grows, you'll want to ensure consistency, stronger typing, meaningful error codes that clearly describe what happened, localization, whether it's a 200 OK or a 503 Service Unavailable, and comprehensive testing to build confidence in your implementation.

That's when the quick way starts feeling fragile. You're copy-pasting response formatting across endpoints. Validation logic is scattered. Your controllers grow to 100+ lines with no clear separation between HTTP concerns and business logic.

Testing becomes tedious because you’re asserting against arrays with unknown shapes. The properties and types are unclear until you read the implementation. These aren’t problems with the quick approach itself. It’s not wrong. It’s just not enough anymore. In other words, it’s simply not scalable.

The Scalable Way

// app/Http/Controllers/HealthController.php
final readonly class HealthController
{
    public function __invoke(): Responsable
    {
        return new ModelResponse(
            data: new HealthResource(
                resource: HealthDTOMapper::toDTO(...),
            )
        );
    }
}

The difference is the use of Response Wrappers via Responsable responses, resources, DTOs, and mappers (or transformers, as they’re sometimes called). There’s no magic involved. Instead of returning raw responses directly from controllers, this approach introduces a structured layer: responses implement a common contract, data is wrapped in consistent response objects, output is transformed through resources, and raw values are mapped into typed DTOs. The result is a predictable, testable, and type‑safe API where formatting, structure, transformation, and localization are clearly separated from controller logic.

These aren't academic exercises. They're practical solutions refined through real production experience. Your controllers stay thin and focused. Your API consumers write once and reuse forever. A new developer looks at one endpoint and understands all of them. That's what we'll discuss throughout this book because that's what separates a quick prototype from a real production API.