
Effortless Laravel & Inertia Data and Type Sync: DTO Magic
Ali Alizadeh
Fri Mar 14 2025
2 min
Table of Contents
Why ?
Let's face it: backend as the source of truth, frontend as the eager student. We've all been there, reinventing type wheels like we're running a medieval blacksmith shop for data structures.
After a few project-induced brain wrinkles, I believe I've stumbled upon a solution that's less "syncing" and more "harmonious data ballet."
Solution
Enter spatie/laravel-data not just a package, but a full-blown data orchestra. It's got more features than a Swiss Army knife, and we're primarily here for the type generation goodness. Our frontend will simply tap its feet to the backend's type-generated rhythm.
Configuring the Backend
First, let's get the band together:
composer require spatie/laravel-data
php artisan vendor:publish --provider="Spatie\LaravelData\LaravelDataServiceProvider" --tag="data-config"
Next we need an enum transformer called spatie/enums which transforms our enums to typescript enums.
Only if we want to use transform_to_native_enums
in the transformer config Which we do.
composer require spatie/enum
Next we need typescript transformer called spatie/laravel-typescript-transformer to transform our class data to typescript types
composer require spatie/laravel-typescript-transformer
php artisan vendor:publish --tag=typescript-transformer-config
I usually change two things from the default config:
use Spatie\TypeScriptTransformer\Formatters\PrettierFormatter;
[
'output_file' => resource_path('js/@types/generated.d.ts'),
'formatter' => PrettierFormatter::class,
'transform_to_native_enums' => true,
]
Notice we've set the transform_to_native_enums
to true that's why we need to Install spatie/enum
Now, let's compose our PostData
masterpiece:
namespace App\Enums\Post;
enum PostStatus: string
{
case Published = 'PUBLISHED';
case Draft = 'DRAFT';
}
namespace App\Data\Post;
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
use App\Enums\Post\PostStatus;
#[TypeScript]
class PostData extends Data
{
public function __construct(
public int $id,
public string $title,
public string $content,
public PostStatus $status,
) {}
}
That #[TypeScript]
attribute? It's telling the transformer to work its magic.
And conduct the transformation:
php artisan typescript:transform
Using the Data
Let's put PostData
on stage:
namespace App\Http\Controllers;
use App\Models\Post;
use App\Data\Post\PostData;
class PostController extends Controller
{
public function index(): \Inertia\Response
{
$posts = Post::all();
return Inertia::render('posts/Index', [
'posts' => PostData::collect($posts),
]);
}
public function show(Post $post): \Inertia\Response
{
return Inertia::render('posts/Show', [
'post' => PostData::from($post),
]);
}
}
Configuring the Frontend
The best part? Minimal effort. Just embrace the types:
type TPage = {
posts: App.Data.Post.PostData[];
};
Tips and Tricks
Generating types manually? That's like tuning a guitar between every chord. Let's automate with a pre-commit hook using husky:
npx husky init && npm i
And in .husky/pre-commit
:
php artisan typescript:transform
For extra credit, let's add type checking in package.json
:
"ts:check": "tsc --project tsconfig.json --noEmit"
and add this command to the pre-commit file
npm run ts:check
So in the end we have:
php artisan typescript:transform
npm run ts:check
Two birds, one stone – type generation and type safety, all before your code even leaves the nest. Now that's what I call a productive pre-commit party!
More Blogs

Simplifying Laravel & Inertia React Forms: A Custom Wrapper
Enhance your Laravel and Inertia React form development with our custom wrapper, eliminating repetitive code and streamlining form management.