In the previous article, we built the foundation for what I called Laravel Blade Partial API Pattern. The idea was simple—to make every Blade partial directly accessible through an API-style URL using HTMX, without writing individual controller methods or route definitions.

That worked fine until we hit one problem: data.

The route knew how to locate and render a partial, but it didn’t know what data that partial needed. Some partials required a single record (like a product card), others needed multiple records (like a product list), and a few needed data built from complex DTO classes. This article is about that missing piece—how to fetch and pass the correct data automatically.


The Problem Recap

The old global route could do this much:

                                /products/partials/product-card/2
                            

→ find resources/views/products/partials/product-card.blade.php

→ load view

→ done

That’s it.

It didn’t care whether that partial needed one product, a list of products, or a composed object with multiple models. We’re now going to fix that.


The Rules (The Contract Between View and Data)

To make the system predictable and maintainable, there need to be rules. After experimenting, I came up with six clear steps that define how a partial route resolves data.

  1. Find the correct partial to load Every request like /products/partials/product-card/2 should map directly to resources/views/products/partials/product-card.blade.php.
  2. Find the resource from the first segment The first segment products is always the model reference. It’s converted to singular form product to locate the model: App\Models\Product.
  3. Decide if one or more records are required This comes from the filename prefix. Starts with products- → multiple records Starts with product- → single record
  4. Extract the DTO or ViewModel annotation At the top of each partial, I can define what data structure it expects:/** @var \App\ViewModels\ProductCardDTO $model */. This tells the route what class to instantiate and which columns the AQC should select.
  5. Fetch the data using AQC Once the DTO is known, the system calls the AQC Design Pattern class to select the appropriate columns and fetch records from the corresponding model.
  6. Return the rendered HTML Finally, the resolved data is passed to the partial and returned as raw HTML — ideal for HTMX swaps or inline updates.

Example in Action

Let’s take these partials:

                                /resources/views/products/partials/products-list.blade.php  // multiple
/resources/views/products/partials/product-card.blade.php   // single
/resources/views/products/partials/product-row.blade.php    // single
/resources/views/products/partials/product-quick-view.blade.php // single
                            

And these URLs:

                                /products/partials/products-list
/products/partials/product-card/2
/products/partials/product-row/2
/products/partials/product-quick-view/2
                            

Here’s the global route refactored.

                                use App\Helpers\PartialApiResolver;

Route::match(['get', 'post'], '{path}', function(string $path, Request $request){
    return App\Helpers\PartialApiResolver::handle($path, $request);
})->where('path', '.*');
                            

And here's the helper class with all 6 steps.

                                <?php
namespace App\Helpers;

use Illuminate\Http\Request;
use Illuminate\Support\Str;

class PartialApiResolver
{
    public static function handle(string $path, Request $request)
    {
        $path = trim($path, '/');
        if ($path === '') abort(404);

        $segments = explode('/', $path);
        $id = self::extractId($segments);
        $viewPath = self::viewPath($segments);
        $resourceName = self::detectResource($segments);
        $dtoClass = self::extractDto($segments);
        $data = self::fetchData($resourceName, $id, $dtoClass, $request);

        return view($viewPath, $data);
    }

    private static function extractId(array &$segments): ?int
    {
        $maybeLast = end($segments);
        if (is_numeric($maybeLast)) {
            array_pop($segments);
            return (int) $maybeLast;
        }
        return null;
    }

    private static function viewPath(array $segments): string
    {
        return implode('.', $segments);
    }

    private static function detectResource(array $segments): ?string
    {
        $resourceSegment = $segments[0] ?? null;
        return $resourceSegment ? Str::singular($resourceSegment) : null;
    }

    private static function extractDto(array $segments): ?string
    {
        $bladeFile = resource_path('views/' . implode('/', $segments) . '.blade.php');
        if (!file_exists($bladeFile)) return null;

        $contents = file_get_contents($bladeFile);
        if (preg_match('/@var\s+([A-Za-z0-9_\\\\<>]+)\s+\$[A-Za-z0-9_]+/m', $contents, $m)) {
            return trim($m[1]);
        }
        return null;
    }

    private static function fetchData(?string $resource, ?int $id, ?string $dto, Request $request): array
    {
        if (!$resource) return [];
        $aqcNamespace = "App\\AQC\\" . ucfirst($resource);
        $columns = $dto && method_exists($dto, 'columns') ? $dto::columns() : '*';

        try {
            if ($id) {
                $aqcClass = "{$aqcNamespace}\\Get" . ucfirst($resource);
                if (!class_exists($aqcClass)) abort(404, "AQC class [$aqcClass] not found.");

                $item = $aqcClass::handle($id, $request->all(), $columns);
                return [$resource => $item ?: new ("App\\Models\\" . ucfirst($resource))];
            } else {
                $aqcClass = "{$aqcNamespace}\\Get" . Str::plural($resource);
                if (!class_exists($aqcClass)) abort(404, "AQC class [$aqcClass] not found.");

                $items = $aqcClass::handle($request->all(), $columns);
                return [Str::plural($resource) => $items];
            }
        } catch (\Throwable $e) {
            return $id
                ? [$resource => new ("App\\Models\\" . ucfirst($resource))]
                : [Str::plural($resource) => collect()];
        }
    }
}
                            

Implementation Notes

Here's what we have done so far.

  • Detect plural or singular pattern to decide between collection or single model.
  • Check for numeric ID in the URL (if found, load a single record).
  • Parse the partial file to extract the DTO or ViewModel class name from annotations.
  • Pass model and DTO into your AQC pipeline to build queries dynamically.
  • Graceful fallback: if a record is missing, provide a new model instance instead of throwing an error.

This way, even if you hit a partial like /product/partials/product-card/9999, it still renders without breaking the flow.


Why This Works So Well

Because it combines the predictability of Laravel’s Blade conventions with the flexibility of APIs. HTMX requests behave like AJAX calls but stay fully server-driven. Your AQC pattern handles the data efficiently. And your Blade partials now serve as self-contained, declarative view components that know what data they need.

You don’t have to touch controllers, define routes, or mix JSON and HTML responses. The system just knows what to do.


Final Thoughts

This pattern now feels complete. The first part gave partials their own routes. This part gave them data.

What you have now is a unified layer where every Blade partial can act like an API endpoint — with model discovery, DTO integration, and automatic data fetching.

This is what I have experienced recently. What is your take on this approach? Let's hear your ideas and thoughts.

Comments


Comment created and will be displayed once approved.