DEV Community

Cover image for Laravel Blade Partial API Pattern: Fetching Data — The Missing Part
Raheel Shan
Raheel Shan

Posted on • Originally published at raheelshan.com

Laravel Blade Partial API Pattern: Fetching Data — The Missing Part

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
Enter fullscreen mode Exit fullscreen mode

And these URLs:

/products/partials/products-list 
/products/partials/product-card/2
/products/partials/product-row/2 
/products/partials/product-quick-view/2
Enter fullscreen mode Exit fullscreen mode

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', '.*');
Enter fullscreen mode Exit fullscreen mode

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()];
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that you know how we load partials, let’s look at how we prepare their data at a structural level.

Note before you move on

To fully understand why I’m using DTO classes and the Atomic Query Construction (AQC) pattern in this setup, don’t skip this part. Both are essential to how the Partial API pattern works under the hood. DTOs define the structure of data your Blade partial expects, while AQC defines how that data is fetched. The rest of this article will make a lot more sense once you grasp both pieces—so read carefully.  

Using DTO Classes with Blade Partials

In this pattern, every Blade partial should know exactly what data structure it’s getting — and that’s where DTO classes come in. Instead of passing a random collection of variables from your controller or resolver, define a dedicated DTO (or ViewModel) that represents the data contract for that specific partial. For example, a ProductCardDTO might expose only name, price, and imageUrl. This makes your Blade file predictable, testable, and IDE-friendly. If you’ve never structured your Blade files around contracts before, you can read my detailed explanation here: 👉 Don’t Pass Array or Variables to Laravel Blade Views Instead Do This

That article shows how to make each Blade file behave like a component with a well-defined interface, keeping logic where it belongs and leaving the template clean. Once you apply that mindset, the Partial API pattern becomes far more maintainable because every endpoint and partial communicates through a stable contract instead of loose variables.

The Role of the AQC Design Pattern

Behind the DTOs sits the Atomic Query Construction (AQC) pattern. AQC lets you build reusable, composable query classes that encapsulate all data-fetching logic. Instead of sprinkling Model::query() all over your project, each AQC class handles one atomic concern — fetching users, filtering products, calculating stats — and can be reused across controllers, APIs, and partials.

When you combine AQC with DTOs, the flow becomes clean and layered: AQC → DTO → Blade Partial. Your AQC class produces the data. The DTO shapes it. The partial displays it.

You can explore the full pattern in this article: 👉 Introducing the Atomic Query Construction (AQC) Design Pattern

That post dives deeper into why atomic queries outperform ad-hoc query logic and how they integrate naturally with the Partial API pattern described here.

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.

Top comments (0)