phpmystic / laravel-trpc
tRPC for Laravel
Requires
- php: ^8.1
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
README
Bring the end-to-end typesafe API experience of tRPC to your Laravel applications.
Laravel tRPC allows you to build APIs in PHP with the same developer experience as tRPC in the TypeScript ecosystem. Define your procedures in Laravel, and automatically generate TypeScript types for your frontend client.
Features
- ✅ Full TypeScript Safety: Synchronize your backend routes and frontend types instantly.
- ✅ Fluent Action Classes: Keep your logic clean with dedicated procedure classes.
- ✅ Artisan Helpers: Quickly scaffold new procedure classes with
php artisan make:trpc. - ✅ Native Laravel Integration: Works seamlessly with Laravel Validation, FormRequests, Auth, and Request context.
- ✅ tRPC Protocol Compliant: Supports single requests, batching (
?batch=1), and standard error formatting. - ✅ Automatic Type Generation: Reflection-based return type inference and Zod schema generation.
Installation
composer require phpmystic/laravel-trpc
Then, publish the configuration file:
php artisan vendor:publish --tag=trpc-config
Quick Start
1. Define your Procedures
You can define procedures using dedicated Action Classes (recommended) or Closures.
Option A: Action Classes
namespace App\Trpc; class ListUsers { /** * Define validation rules (optional) */ public function rules(): array { return [ 'search' => 'nullable|string', ]; } /** * Handle the procedure logic */ public function handle(array $input) { return \App\Models\User::where('name', 'like', "%{$input['search']}%")->get(); } }
Option B: Inline Closures
use Phpmystic\Trpc\Facades\Trpc; Trpc::router('ping') ->query('hello', function (array $input) { return "Hello " . ($input['name'] ?? 'World'); });
2. Register Routers
Register your procedures in a route file (e.g., routes/web.php or a dedicated routes/trpc.php).
use Phpmystic\Trpc\Facades\Trpc; use App\Trpc\ListUsers; use App\Trpc\StoreUser; Trpc::router('users') ->query('list', ListUsers::class) ->mutation('store', StoreUser::class);
3. Setup the HTTP Endpoint
Map the tRPC controller to a route:
Route::any('/trpc/{path}', \Phpmystic\Trpc\Controllers\TrpcController::class) ->where('path', '.*');
4. Generate TypeScript Types
Run the artisan command to generate the _app.ts router file for your frontend:
php artisan trpc:generate --output=resources/ts/trpc/_app.ts
High Performance Watcher
Instead of running the command manually every time you make a change, you can use the --watch flag to automatically keep your types in sync:
php artisan trpc:generate --watch
The watcher is optimized to use minimal CPU and only regenerates when it detects changes in your procedure classes or route files.
Scaffolding
You can quickly create a new tRPC Action Class using the artisan helper:
php artisan make:trpc ListUsers
This will create a new file in app/Trpc/ListUsers.php with the standard rules() and handle() methods.
Frontend Usage
1. Install Dependencies
In your frontend project (React, Vue, etc.), install the tRPC client:
npm install @trpc/client @trpc/server zod
2. Configure the Client
Import the AppRouter type generated by the artisan command to initialize your client.
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './trpc/_app'; // Path to generated file const trpc = createTRPCProxyClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://your-laravel-app.test/trpc', }), ], });
3. Usage with Full Type Safety
// Queries const users = await trpc.users.list.query({ search: 'John' }); // Mutations const newUser = await trpc.users.store.mutate({ name: 'Jane Doe', email: 'jane@example.com' });
4. Handling Validation Errors
Laravel validation errors are automatically mapped to a format compatible with Zod's fieldErrors.
try { await trpc.users.store.mutate({ email: 'invalid' }); } catch (error) { if (error.data?.zodError) { // { email: ['The email field must be a valid email address.'] } const fieldErrors = error.data.zodError.fieldErrors; console.log(fieldErrors); } }
Advanced Usage
Context and Middleware
Procedures have access to the Context, which contains the current Request and User.
Trpc::router('secure') ->query('secretData', function ($input, $ctx) { return "Secret for user: " . $ctx->getUser()->name; }) ->use(function ($ctx, $next) { if (!$ctx->getUser()) { abort(401); } return $next($ctx); });
Native FormRequests
You can pass a Laravel FormRequest class directly to the input() method.
Trpc::router('profile') ->mutation('update', UpdateProfileAction::class) ->input(\App\Http\Requests\UpdateProfileRequest::class);
Explicit Output Typing
While the package uses reflection to infer return types, you can override them manually:
Trpc::router('stats') ->query('count', fn() => 100) ->output('number'); // Explicitly set TS type to number
Configuration
Publish the configuration file to customize the package behavior:
php artisan vendor:publish --tag=trpc-config
In config/trpc.php, you can:
- Change the default TypeScript output path.
- Define Global Middleware that runs on every tRPC request.
- Toggle stack traces in error responses.
Data Transformation
Laravel tRPC automatically transforms common Laravel objects into tRPC-friendly formats:
- Carbon / DateTime: Automatically formatted as ISO 8601 strings.
- Eloquent Models & Collections: Automatically converted to serializable arrays.
- Paginators: Transformed into a standard structure:
{
"items": [...],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 15,
"total": 75
}
}
License
The MIT License (MIT). Please see License File for more information.