| Install | |
|---|---|
composer require oybek-daniyarov/laravel-trpc |
|
| Latest Version: | v0.1.10-beta |
| PHP: | ^8.3 |
Generate a fully typed TypeScript client from your Laravel routes — with request types, response types, and optional React Query/Inertia helpers.
import { createApi } from '@/api';
const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_URL!,
});
// Full autocomplete and type safety
const users = await api.users.index();
const user = await api.users.show({ user: 1 });
const newUser = await api.users.store({
body: { name: 'John', email: 'john@example.com', password: 'secret' }
});

What it is: A Laravel-first generator that inspects your routes and produces a TypeScript client with fully typed inputs and outputs.
What it isn't: This is not a server-side implementation of the tRPC protocol. It doesn't require a Node.js tRPC router. It generates a typed HTTP client for your existing Laravel routes.
Runtime model: Generation happens at build time via php artisan trpc:generate. The output is a small TypeScript client that calls your HTTP endpoints — no runtime reflection on the PHP side.
Beta (0.x). The public API and generated output structure may change between minor versions. For production use:
composer.jsonapi.users.show()) with full autocompleteSee it in action — a Laravel API with Inertia.js frontend, fully typed end-to-end:
🔗 github.com/oybek-daniyarov/empty-space
composer require spatie/laravel-data spatie/laravel-typescript-transformer
composer require oybek-daniyarov/laravel-trpc
Publish the config file:
php artisan vendor:publish --tag=trpc-config
use Spatie\LaravelData\Data;
use Spatie\TypeScriptTransformer\Attributes\TypeScript;
#[TypeScript]
class UserData extends Data
{
public function __construct(
public int $id,
public string $name,
public string $email,
) {}
}
#[TypeScript]
class CreateUserData extends Data
{
public function __construct(
public string $name,
public string $email,
public string $password,
) {}
}
use OybekDaniyarov\LaravelTrpc\Attributes\TypedRoute;
class UserController extends Controller
{
#[TypedRoute(response: UserData::class, isPaginated: true)]
public function index()
{
return UserData::collect(User::paginate());
}
#[TypedRoute(response: UserData::class)]
public function show(User $user)
{
return UserData::from($user);
}
#[TypedRoute(request: CreateUserData::class, response: UserData::class)]
public function store(CreateUserData $data)
{
return UserData::from(User::create($data->toArray()));
}
#[TypedRoute(request: UpdateUserData::class, response: UserData::class)]
public function update(User $user, UpdateUserData $data)
{
$user->update($data->toArray());
return UserData::from($user);
}
#[TypedRoute]
public function destroy(User $user)
{
$user->delete();
return response()->noContent();
}
}
The #[TypedRoute] attribute explicitly declares request and response types for your API endpoints. When applied, it takes priority over static analysis for type detection.
| Parameter | Type | Description |
|---|---|---|
request |
class-string|null |
Request body Data class (for POST/PUT/PATCH) |
query |
class-string|null |
Query parameters Data class (for GET requests) |
response |
class-string|null |
Response Data class |
errorResponse |
class-string|null |
Error response Data class (defaults to ValidationError) |
isCollection |
bool |
Response is an array of items (Array<T>) |
isPaginated |
bool |
Response is paginated (PaginatedResponse<T>) |
Basic response type:
#[TypedRoute(response: UserData::class)]
public function show(User $user)
{
return UserData::from($user);
}
Request and response types:
#[TypedRoute(request: CreateUserData::class, response: UserData::class)]
public function store(CreateUserData $data)
{
return UserData::from(User::create($data->toArray()));
}
Query parameters (for GET with filters/search):
#[TypedRoute(query: UserFilterData::class, response: UserData::class, isPaginated: true)]
public function index(UserFilterData $filters)
{
return UserData::collect(
User::filter($filters)->paginate()
);
}
Paginated response:
#[TypedRoute(response: UserData::class, isPaginated: true)]
public function index()
{
return UserData::collect(User::paginate());
}
// TypeScript: PaginatedResponse<UserData>
Collection response (non-paginated array):
#[TypedRoute(response: UserData::class, isCollection: true)]
public function all()
{
return UserData::collect(User::all());
}
// TypeScript: Array<UserData>
No response body (204 No Content):
#[TypedRoute]
public function destroy(User $user)
{
$user->delete();
return response()->noContent();
}
// TypeScript: void
Custom error response:
#[TypedRoute(
request: CreateUserData::class,
response: UserData::class,
errorResponse: CreateUserErrorData::class
)]
public function store(CreateUserData $data)
{
// ...
}
request for body data (POST, PUT, PATCH requests)query for URL query parameters (GET requests with filters, search, pagination)// GET /api/users?status=active&sort=name
#[TypedRoute(query: UserFilterData::class, response: UserData::class, isPaginated: true)]
public function index(UserFilterData $filters) { }
// POST /api/users (body: { name: "John", email: "john@example.com" })
#[TypedRoute(request: CreateUserData::class, response: UserData::class)]
public function store(CreateUserData $data) { }
php artisan trpc:generate
import { api } from '@/lib/api';
// Full autocomplete and type safety
const users = await api.users.index();
const user = await api.users.show({ user: 1 });
const newUser = await api.users.store({
body: { name: 'John', email: 'john@example.com', password: 'secret' }
});
await api.users.update({ user: 1, body: { name: 'Jane' } });
await api.users.destroy({ user: 1 });
The generator produces a tree-shakeable folder structure:
resources/js/api/
├── core/ # Core infrastructure
│ ├── types.ts # HttpMethod, ApiError, PaginatedResponse, ValidationError
│ ├── fetch.ts # Low-level fetch wrapper with full type safety
│ ├── helpers.ts # Type helpers (RequestOf, ResponseOf, ParamsOf, QueryOf)
│ └── index.ts # Core barrel exports
├── {group}/ # Per-resource folders (users/, posts/, etc.)
│ ├── routes.ts # Group-specific route definitions
│ ├── api.ts # createUsersApi() factory
│ ├── queries.ts # createUsersQueries() factory (optional)
│ ├── mutations.ts # createUsersMutations() factory (optional)
│ └── index.ts # Group barrel exports
├── routes.ts # Aggregated route definitions
├── api.ts # createApi() factory combining all groups
├── queries.ts # createQueries() factory (optional)
├── mutations.ts # createMutations() factory (optional)
├── url-builder.ts # Type-safe URL builder
├── client.ts # Method-specific client (client.get(), etc.)
├── inertia.ts # Inertia.js helpers (optional)
├── react-query.ts # React Query utilities (optional)
├── index.ts # Main barrel exports
└── README.md # Generated documentation
Import only what you need for optimal bundle size:
// Per-resource import (tree-shakeable) - only imports users code
import { createUsersApi } from '@/api/users';
// Or combined API - imports all resources
import { createApi } from '@/api';
The generator produces a stable folder structure:
resources/js/api/
├── core/ # Core infrastructure (always generated)
│ ├── types.ts # ApiError, PaginatedResponse, ValidationError
│ ├── fetch.ts # Low-level fetch wrapper
│ ├── helpers.ts # Type helpers (RequestOf, ResponseOf, etc.)
│ └── index.ts # Core exports
├── {group}/ # Per-resource folders (e.g., users/, posts/)
│ ├── routes.ts # Route definitions for this group
│ ├── api.ts # createUsersApi() factory
│ ├── queries.ts # createUsersQueries() (if react-query enabled)
│ └── index.ts # Group exports
├── routes.ts # Aggregated routes from all groups
├── api.ts # createApi() factory
├── queries.ts # createQueries() factory (optional)
├── url-builder.ts # Type-safe URL builder
├── client.ts # Method-specific client
├── inertia.ts # Inertia.js helpers (optional)
├── react-query.ts # React Query utilities (optional)
├── index.ts # Barrel exports
└── README.md # Generated documentation
users.index → api.users.index(){user} → { user: number }// config/trpc.php
return [
// Output directory for generated files
'output_path' => resource_path('js/api'),
// API route prefix filter
'api_prefix' => 'api',
// Route collection mode: 'api', 'web', 'all', 'named', 'attributed'
'route_mode' => 'api',
// Exclude patterns
'exclude_patterns' => [
'debugbar.*',
'horizon.*',
'telescope.*',
],
// Output files to generate
'outputs' => [
'routes' => true,
'types' => true,
'helpers' => true,
'url-builder' => true,
'fetch' => true,
'client' => true,
'index' => true,
'readme' => true,
'grouped-api' => true,
'inertia' => true,
'react-query' => false,
'queries' => false,
'mutations' => false,
],
];
| Option | Type | Default | Description |
|---|---|---|---|
output_path |
string | resource_path('js/api') |
Directory for generated TypeScript files |
api_prefix |
string | 'api' |
API route prefix for filtering |
version |
string | 'v1' |
API version identifier |
route_mode |
string | 'api' |
Route collection mode (see below) |
include_patterns |
array | [] |
Route patterns to include |
exclude_patterns |
array | [...] |
Route patterns to exclude |
exclude_methods |
array | ['options', 'head'] |
HTTP methods to skip |
preset |
string|null | null |
Framework preset: 'inertia', 'api', 'spa' |
outputs |
array | [...] |
Files to generate |
auto_typescript_transform |
bool | true |
Auto-run typescript:transform |
laravel_types_path |
string|null | null |
Path to laravel.d.ts |
| Mode | Description |
|---|---|
'api' |
Only routes starting with api_prefix (default) |
'web' |
Only routes NOT starting with api_prefix |
'all' |
All routes (use with include/exclude patterns) |
'named' |
Only routes with names |
'attributed' |
Only routes with #[TypedRoute] attribute |
Use presets to quickly enable common output configurations:
// config/trpc.php
return [
'preset' => 'spa', // 'inertia', 'api', or 'spa'
];
| Preset | Enables | Use Case |
|---|---|---|
'inertia' |
Core files + Inertia helpers | Laravel + Inertia.js apps |
'api' |
Core files + React Query + Mutations | API-first / SPA with React Query |
'spa' |
Core files + Inertia + React Query + Mutations | Full-featured SPA |
null |
Custom (configure outputs manually) |
Fine-grained control |
Presets override the outputs array. To customize individual outputs, set preset to null and configure outputs directly.
'postman' => [
'output_path' => storage_path('app/postman'),
'collection_name' => env('APP_NAME', 'API').' Collection',
'base_url' => '{{base_url}}',
'auth_type' => 'bearer', // 'bearer', 'apikey', or null
'default_headers' => [],
],
Control how middleware appears in the generated TypeScript output:
'middleware' => [
// Exclude middleware from generated output (supports wildcards)
'exclude' => [
'Stancl\Tenancy\*',
'App\Http\Middleware\TrustProxies',
],
// Transform FQCNs to short class names (default: true)
'short_names' => true,
],
Before (with short_names: false and no exclusions):
middleware: ['api', 'Stancl\\Tenancy\\Middleware\\InitializeTenancyByDomain', 'auth:sanctum'] as const,
After (with short_names: true and Tenancy excluded):
middleware: ['api', 'auth:sanctum'] as const,
| Middleware Input | Short Name Output |
|---|---|
Stancl\Tenancy\Middleware\InitializeTenancyByDomain |
InitializeTenancyByDomain |
App\Http\Middleware\RateLimiter:api |
RateLimiter:api |
auth:sanctum |
auth:sanctum (unchanged) |
web |
web (unchanged) |
The generated routes include middleware information, allowing you to build auth-aware UIs.
Each route includes middleware and authentication info:
// In routes.ts
export const routes = {
'users.index': {
path: 'api/users',
method: 'get',
params: [],
middleware: ['auth:sanctum', 'verified'] as const,
authenticated: true,
},
'auth.login': {
path: 'api/auth/login',
method: 'post',
params: [],
middleware: [] as const,
authenticated: false,
},
} as const;
Filter routes by authentication requirement:
import type { AuthenticatedRoutes, PublicRoutes } from '@/api';
// Only routes that require authentication
type ProtectedRoutes = AuthenticatedRoutes;
// 'users.index' | 'users.store' | 'users.update' | ...
// Only routes that don't require authentication
type OpenRoutes = PublicRoutes;
// 'auth.login' | 'auth.register' | ...
import { routes, type RouteName } from '@/api';
function isAuthRequired(name: RouteName): boolean {
return routes[name].authenticated;
}
// Use in components
function ApiButton({ route, children }: { route: RouteName; children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
if (routes[route].authenticated && !isAuthenticated) {
return <LoginPrompt />;
}
return <button onClick={() => callApi(route)}>{children}</button>;
}
import { routes } from '@/api';
// Get middleware for a route
const middleware = routes['users.index'].middleware;
// ['auth:sanctum', 'verified']
// Check for specific middleware
const requiresVerification = middleware.includes('verified');
The generated types.ts includes standard Laravel error types:
// Base API error (thrown by fetch wrapper)
interface ApiError {
readonly message: string;
readonly status: number; // HTTP status code
readonly statusText?: string; // HTTP status text
readonly errors?: Record<string, string[]>; // Validation errors
}
// Specific error types
interface ValidationError { message: string; errors: Record<string, string[]>; }
interface NotFoundError { message: string; }
interface UnauthorizedError { message: string; }
interface ForbiddenError { message: string; }
interface ServerError { message: string; exception?: string; trace?: [...]; }
errorResponseDefine custom error Data classes for specific routes:
// Define a custom error type
#[TypeScript]
class CreateUserErrorData extends Data
{
public function __construct(
public string $message,
public ?string $email_suggestion, // Custom field
public ?array $password_requirements,
) {}
}
// Use in controller
#[TypedRoute(
request: CreateUserData::class,
response: UserData::class,
errorResponse: CreateUserErrorData::class // Custom error type
)]
public function store(CreateUserData $data)
{
// If validation fails, return custom error structure
if (User::where('email', $data->email)->exists()) {
return response()->json([
'message' => 'Email already taken',
'email_suggestion' => $data->email . '.new',
], 422);
}
return UserData::from(User::create($data->toArray()));
}
import type { ErrorOf, ApiError } from '@/api';
import { api } from '@/lib/api';
// Get the error type for a specific route
type CreateUserError = ErrorOf<'users.store'>;
// CreateUserErrorData (custom) or ValidationError (default)
// Handle errors with proper typing
async function createUser(data: CreateUserData) {
try {
return await api.users.store({ body: data });
} catch (e) {
const error = e as ApiError;
console.log(error.status); // 422
console.log(error.message); // "Email already taken"
console.log(error.errors); // { email: ["Email already taken"] }
// For custom error fields, cast to specific type
if (error.status === 422) {
const customError = error as unknown as CreateUserError;
console.log(customError.email_suggestion);
}
}
}
import { useMutation } from '@tanstack/react-query';
import type { ErrorOf, ApiError } from '@/api';
function CreateUserForm() {
const mutation = useMutation({
mutationFn: (data: CreateUserData) => api.users.store({ body: data }),
onError: (error: ApiError) => {
if (error.status === 422 && error.errors) {
// Show field-specific errors
Object.entries(error.errors).forEach(([field, messages]) => {
setFieldError(field, messages[0]);
});
} else if (error.status === 401) {
redirectToLogin();
}
},
});
// ...
}
Extract types from route names for use in your components:
import type { RequestOf, ResponseOf, ParamsOf, QueryOf, ErrorOf } from '@/api';
// Request body type
type CreateUserPayload = RequestOf<'users.store'>;
// Response type
type UserResponse = ResponseOf<'users.show'>;
// URL parameters type
type UserParams = ParamsOf<'users.show'>; // { user: number }
// Query parameters type
type UserQuery = QueryOf<'users.index'>; // { page?: number, per_page?: number }
// Error type (custom or ValidationError)
type StoreUserError = ErrorOf<'users.store'>;
Enable in config:
'outputs' => [
'react-query' => true, // Core utilities (queryKey, createQueryOptions)
'queries' => true, // Resource-based query hooks (usersQueries, etc.)
'mutations' => true, // Resource-based mutation hooks (usersMutations, etc.)
],
| File | Description |
|---|---|
react-query.ts |
Low-level utilities: queryKey, createQueryOptions, createInfiniteQueryOptions |
queries.ts |
Resource-based query factories organized by API resource (e.g., usersQueries, postsQueries) |
mutations.ts |
Resource-based mutation factories organized by API resource (e.g., usersMutations, postsMutations) |
Before using queries and mutations, create configured instances in a setup file:
// lib/api.ts
import { createApi, createQueries, createMutations } from '@/api';
const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '',
headers: { 'X-App-Version': '1.0.0' },
});
export const queries = createQueries(api);
export const mutations = createMutations(api);
export { api };
Then import from your setup file in components:
import { api, queries, mutations } from '@/lib/api';
queries.ts)The queries.ts file generates query factories for each API resource, providing:
queryOptions() and infiniteQueryOptions() for each GET endpointkeys object for cache managementimport { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import { queries } from '@/lib/api';
// Simple query
const { data: user } = useQuery(queries.users.show({ user: 1 }));
// Paginated endpoints automatically use infinite queries
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery(
queries.users.index({ query: { per_page: 20 } })
);
// Query keys for cache invalidation
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: queries.users.keys.all }); // ['users']
queryClient.invalidateQueries({ queryKey: queries.users.keys.show({ user: 1 }) }); // ['users', 'show', { user: 1 }]
react-query.ts)For more control, use the low-level utilities:
import { useQuery } from '@tanstack/react-query';
import { queryKey, createQueryOptions } from '@/api';
// Create query options manually
const { data } = useQuery(
createQueryOptions('users.show', {
path: { user: 1 },
staleTime: 5000,
})
);
// Query keys for cache management
const key = queryKey('users.show', { path: { user: 1 } });
// ['users.show', { user: 1 }]
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { queries, api } from '@/lib/api';
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading } = useQuery(queries.users.show({ user: userId }));
const queryClient = useQueryClient();
const updateUser = useMutation({
mutationFn: (data: { name: string }) =>
api.users.update({ user: userId, body: data }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queries.users.keys.show({ user: userId })
});
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{data?.name}</h1>
<button onClick={() => updateUser.mutate({ name: 'New Name' })}>
Update
</button>
</div>
);
}
Paginated endpoints automatically generate infiniteQueryOptions:
import { useInfiniteQuery } from '@tanstack/react-query';
import { queries } from '@/lib/api';
function UserList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery(
queries.users.index({ query: { per_page: 20 } })
);
return (
<div>
{data?.pages.flatMap(page => page.data).map(user => (
<div key={user.id}>{user.name}</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
);
}
mutations.ts)The mutations.ts file generates type-safe mutation factories for POST, PUT, PATCH, and DELETE endpoints:
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createApi, createMutations, createQueries } from '@/api';
const api = createApi({ baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '' });
const mutations = createMutations(api);
const queries = createQueries(api);
function CreateUserButton() {
const queryClient = useQueryClient();
const createUser = useMutation({
...mutations.users.store(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queries.users.keys.all });
},
});
// TypeScript enforces body is REQUIRED for store route
return (
<button
onClick={() => createUser.mutate({
body: { name: 'John', email: 'john@example.com' }
})}
disabled={createUser.isPending}
>
Create User
</button>
);
}
function DeleteUserButton({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const deleteUser = useMutation({
...mutations.users.destroy(),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queries.users.keys.all });
},
});
// TypeScript allows NO body for destroy route (only path params)
return (
<button onClick={() => deleteUser.mutate({ user: userId })}>
Delete
</button>
);
}
Mutation keys for cache management:
mutations.users.keys.all // ['users', 'mutation']
mutations.users.keys.store() // ['users.store']
mutations.users.keys.update() // ['users.update']
mutations.users.keys.destroy() // ['users.destroy']
import { router } from '@inertiajs/react';
import { route, visit, formAction } from '@/api/inertia';
// Generate type-safe URLs
const url = route('users.show', { user: 123 });
// Navigate with type safety
visit('users.show', { user: 123 });
visit('users.index', null, { query: { page: 2 } });
// Form actions
function CreateUserForm() {
return (
<form {...formAction('users.store')}>
<input name="name" />
<input name="email" type="email" />
<button type="submit">Create</button>
</form>
);
}
Create a configured API instance and export it for use throughout your app:
// lib/api.ts
import { createApi } from '@/api';
export const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '',
headers: { 'X-App-Version': '1.0.0' },
});
import { api } from '@/lib/api';
// Simple usage
const users = await api.users.index();
// With custom headers (per-request)
const users = await api.users.index({
headers: { 'X-Custom': 'value' },
});
// With path params
const user = await api.users.show({ user: 1 });
// With path params and per-request options
const user = await api.users.show({ user: 1 }, {
headers: { 'X-Request-Id': 'abc123' },
});
// With body (POST/PUT/PATCH)
const newUser = await api.users.store({ body: { name: 'John', email: 'john@example.com' } });
All API methods accept an optional RequestOptions object as the last parameter:
interface RequestOptions {
headers?: Record<string, string>; // Custom headers for this request
next?: NextCacheOptions; // Next.js cache configuration
mobile?: MobileOptions; // Mobile/React Native options
signal?: AbortSignal; // Abort signal
}
// lib/api.server.ts
import { createApi } from '@/api';
import { cookies } from 'next/headers';
import { cache } from 'react';
// Cache per request (React cache for deduplication)
export const getServerApi = cache(async () => {
const cookieStore = await cookies();
const token = cookieStore.get('token')?.value;
return createApi({
baseUrl: process.env.API_URL!,
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
});
// app/users/page.tsx
import { getServerApi } from '@/lib/api.server';
export default async function UsersPage() {
const api = await getServerApi();
// With Next.js cache tags for revalidation
const users = await api.users.index({
next: { tags: ['users'], revalidate: 60 }
});
return <UserList users={users.data} />;
}
// app/actions/users.ts
'use server';
import { getServerApi } from '@/lib/api.server';
import { revalidateTag } from 'next/cache';
export async function createUser(data: CreateUserData) {
const api = await getServerApi();
const user = await api.users.store({ body: data });
revalidateTag('users');
return user;
}
export async function deleteUser(userId: number) {
const api = await getServerApi();
await api.users.destroy({ user: userId });
revalidateTag('users');
revalidateTag(`user-${userId}`);
}
// app/api/users/route.ts
import { createApi } from '@/api';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const api = createApi({
baseUrl: process.env.API_URL!,
headers: {
Authorization: request.headers.get('Authorization') ?? '',
},
});
const users = await api.users.index();
return NextResponse.json(users);
}
Use createQueries to bind queries to a configured API instance:
// lib/api.ts
import { createApi, createQueries, createMutations } from '@/api';
const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_URL ?? '',
headers: { 'X-App-Version': '1.0.0' },
});
export const queries = createQueries(api);
export const mutations = createMutations(api);
export { api };
// components/UserProfile.tsx
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { queries, api } from '@/lib/api';
export function UserProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const { data: user, isLoading } = useQuery(
queries.users.show({ user: userId })
);
const updateMutation = useMutation({
mutationFn: (data: UpdateUserData) =>
api.users.update({ user: userId, body: data }),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queries.users.keys.show({ user: userId })
});
},
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user?.name}</h1>
<button onClick={() => updateMutation.mutate({ name: 'New Name' })}>
Update
</button>
</div>
);
}
# Generate TypeScript definitions (default)
php artisan trpc:generate
# Generate Postman collection only
php artisan trpc:generate --postman
# Generate both TypeScript and Postman collection
php artisan trpc:generate --format=all
# Custom output directory (overrides config)
php artisan trpc:generate --output=resources/js/generated
# Override API prefix for route filtering
php artisan trpc:generate --api-prefix=api/v2
# Skip running typescript:transform automatically
php artisan trpc:generate --skip-typescript-transform
# Generate Postman collection with environment file
php artisan trpc:generate --postman --postman-env
# Overwrite files without confirmation
php artisan trpc:generate --force
# Custom base URL for generated client
php artisan trpc:generate --base-url=https://api.example.com
| Option | Description |
|---|---|
--output=PATH |
Override the output directory from config |
--api-prefix=PREFIX |
Override the API route prefix filter |
--skip-typescript-transform |
Skip auto-running typescript:transform |
--postman |
Generate Postman collection only (shorthand for --format=postman) |
--postman-env |
Also generate Postman environment file |
--format=FORMAT |
Output format: typescript (default), postman, or all |
--force |
Overwrite existing files without confirmation |
--base-url=URL |
Set default base URL for the generated TypeScript client |
Publish the stub templates:
php artisan vendor:publish --tag=trpc-stubs
Templates will be copied to resources/views/vendor/trpc/.
number | string by default. Custom types require explicit #[TypedRoute] configuration.#[TypedRoute(response: ...)].auth, auth:*). Custom auth middleware may not be detected.api_prefix (default: api)php artisan route:list to verify routes existunknown#[TypeScript] attribute to your Data classesphp artisan typescript:transform firstlaravel.d.ts exists in your output directoryrm -rf node_modules/.cachetsconfig.json includes the output directory_1, _2 suffixes for duplicate namesroute_name_mappings in configThis package follows Semantic Versioning:
During the 0.x beta period, minor versions may include breaking changes. These will be clearly documented in the CHANGELOG.
I built this to solve a real problem: getting a strongly typed TypeScript client from Laravel routes without manual type duplication.
I used AI tooling (Claude Code) to accelerate development — it helped with boilerplate, iteration, and exploration. The architecture and direction were mine; the implementation was collaborative.
The package has 250+ passing tests, PHPStan level 8 analysis, and is used in production projects. I'm sharing it because it works and saves time.
Found a bug? Open an issue. I'll fix it.
MIT License. See LICENSE for details.