PHPackages                             oybek-daniyarov/laravel-trpc - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. oybek-daniyarov/laravel-trpc

ActiveLibrary[API Development](/categories/api)

oybek-daniyarov/laravel-trpc
============================

End-to-end type-safe APIs for Laravel. Like tRPC, but for Laravel + TypeScript.

v0.2.0-beta(2mo ago)0274MITPHPPHP ^8.3CI failing

Since Jan 21Pushed 2mo agoCompare

[ Source](https://github.com/oybek-daniyarov/laravel-trpc)[ Packagist](https://packagist.org/packages/oybek-daniyarov/laravel-trpc)[ Docs](https://github.com/oybek-daniyarov/laravel-trpc)[ RSS](/packages/oybek-daniyarov-laravel-trpc/feed)WikiDiscussions main Synced today

READMEChangelog (7)Dependencies (20)Versions (15)Used By (0)

Laravel tRPC
============

[](#laravel-trpc)

[![Latest Version on Packagist](https://camo.githubusercontent.com/deedced7f7ad9de3bdcb7c7cfeb0ad6089b879b6de1231dd05f2cdabda2a91dc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f7962656b2d64616e697961726f762f6c61726176656c2d747270632e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oybek-daniyarov/laravel-trpc)[![Total Downloads](https://camo.githubusercontent.com/18e2bf0baa36f5548530439cc3fac58beb1b29e1df670872defa00f9c00ca486/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f7962656b2d64616e697961726f762f6c61726176656c2d747270632e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oybek-daniyarov/laravel-trpc)[![License](https://camo.githubusercontent.com/b4e383bc0b574ac1f38ed8ada7436b2a63d73a8ac6d15b47887fdc8bb915ea2f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6f7962656b2d64616e697961726f762f6c61726176656c2d747270632e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oybek-daniyarov/laravel-trpc)

> Generate a fully typed TypeScript client from your Laravel routes — with request types, response types, and optional React Query/Inertia helpers.

Quick Example
-------------

[](#quick-example)

```
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' }
});
```

[![Autocomplete Demo](docs/autocomplete-demo.gif)](docs/autocomplete-demo.gif)

What This Is (and Isn't)
------------------------

[](#what-this-is-and-isnt)

**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.

Status
------

[](#status)

**Beta (0.x).** The public API and generated output structure may change between minor versions. For production use:

- Pin to exact versions in `composer.json`
- Review generated output diffs on upgrade
- Follow the [CHANGELOG](CHANGELOG.md) for breaking changes

Features
--------

[](#features)

- **Full Type Safety**: Request bodies, responses, URL parameters, and query strings are all typed
- **Zero Runtime Overhead**: Types are generated at build time, no runtime reflection
- **Framework Integrations**: Built-in support for React Query and Inertia.js
- **Grouped API Client**: Object-based API (`api.users.show()`) with full autocomplete
- **Postman Export**: Generate Postman collections from your routes

Example Project
---------------

[](#example-project)

See it in action — a Laravel API with Inertia.js frontend, fully typed end-to-end:

🔗 [github.com/oybek-daniyarov/empty-space](https://github.com/oybek-daniyarov/empty-space)

Table of Contents
-----------------

[](#table-of-contents)

- [Example Project](#example-project)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [TypedRoute Attribute](#typedroute-attribute)
- [Generate &amp; Use](#generate--use)
- [Generated Files](#generated-files)
- [Output Contract](#output-contract)
- [Configuration](#configuration)
- [Middleware &amp; Authentication](#middleware--authentication)
- [Error Handling](#error-handling)
- [Type Helpers](#type-helpers)
- [React Query Integration](#react-query-integration)
- [Inertia.js Integration](#inertiajs-integration)
- [API Client Configuration](#api-client-configuration)
- [Command Options](#command-options)
- [Customizing Stubs](#customizing-stubs)
- [Known Limitations](#known-limitations)
- [Troubleshooting](#troubleshooting)
- [Versioning](#versioning)
- [Backstory](#backstory)

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11.x or 12.x
- [spatie/laravel-data](https://spatie.be/docs/laravel-data)
- [spatie/laravel-typescript-transformer](https://spatie.be/docs/typescript-transformer)

Installation
------------

[](#installation)

```
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
```

Quick Start
-----------

[](#quick-start)

### 1. Define Data Classes

[](#1-define-data-classes)

```
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,
    ) {}
}
```

### 2. Add TypedRoute Attribute to Controllers

[](#2-add-typedroute-attribute-to-controllers)

```
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();
    }
}
```

TypedRoute Attribute
--------------------

[](#typedroute-attribute)

The `#[TypedRoute]` attribute explicitly declares request and response types for your API endpoints. When applied, it takes priority over static analysis for type detection.

### Parameters

[](#parameters)

ParameterTypeDescription`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`)`isPaginated``bool`Response is paginated (`PaginatedResponse`)### Examples

[](#examples)

**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
```

**Collection response (non-paginated array):**

```
#[TypedRoute(response: UserData::class, isCollection: true)]
public function all()
{
    return UserData::collect(User::all());
}
// TypeScript: Array
```

**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)
{
    // ...
}
```

### Query vs Request

[](#query-vs-request)

- Use `request` for **body data** (POST, PUT, PATCH requests)
- Use `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) { }
```

Generate &amp; Use
------------------

[](#generate--use)

### Generate TypeScript Client

[](#generate-typescript-client)

```
php artisan trpc:generate
```

### 4. Use in TypeScript

[](#4-use-in-typescript)

```
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 });
```

Generated Files
---------------

[](#generated-files)

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

```

### Tree-Shaking

[](#tree-shaking)

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';
```

Output Contract
---------------

[](#output-contract)

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

```

### Naming Conventions

[](#naming-conventions)

- **Route names** map directly to TypeScript keys: `users.index` → `api.users.index()`
- **Groups** are derived from the first segment of the route name
- **Parameters** use Laravel's route parameter names: `{user}` → `{ user: number }`

Configuration
-------------

[](#configuration)

```
// 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,
    ],
];
```

### Configuration Reference

[](#configuration-reference)

OptionTypeDefaultDescription`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`### Route Modes

[](#route-modes)

ModeDescription`'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### Presets

[](#presets)

Use presets to quickly enable common output configurations:

```
// config/trpc.php
return [
    'preset' => 'spa', // 'inertia', 'api', or 'spa'
];
```

PresetEnablesUse Case`'inertia'`Core files + Inertia helpersLaravel + Inertia.js apps`'api'`Core files + React Query + MutationsAPI-first / SPA with React Query`'spa'`Core files + Inertia + React Query + MutationsFull-featured SPA`null`Custom (configure `outputs` manually)Fine-grained controlPresets override the `outputs` array. To customize individual outputs, set `preset` to `null` and configure `outputs` directly.

### Postman Configuration

[](#postman-configuration)

```
'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' => [],
],
```

### Middleware Configuration

[](#middleware-configuration)

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 InputShort Name Output`Stancl\Tenancy\Middleware\InitializeTenancyByDomain``InitializeTenancyByDomain``App\Http\Middleware\RateLimiter:api``RateLimiter:api``auth:sanctum``auth:sanctum` (unchanged)`web``web` (unchanged)Middleware &amp; Authentication
-------------------------------

[](#middleware--authentication)

The generated routes include middleware information, allowing you to build auth-aware UIs.

### Generated Route Data

[](#generated-route-data)

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;
```

### Type Helpers for Auth Routes

[](#type-helpers-for-auth-routes)

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' | ...
```

### Checking Auth Before API Calls

[](#checking-auth-before-api-calls)

```
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 ;
    }

    return  callApi(route)}>{children};
}
```

### Accessing Middleware Array

[](#accessing-middleware-array)

```
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');
```

Error Handling
--------------

[](#error-handling)

### Built-in Error Types

[](#built-in-error-types)

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;  // Validation errors
}

// Specific error types
interface ValidationError { message: string; errors: Record; }
interface NotFoundError { message: string; }
interface UnauthorizedError { message: string; }
interface ForbiddenError { message: string; }
interface ServerError { message: string; exception?: string; trace?: [...]; }
```

### Custom Error Types with `errorResponse`

[](#custom-error-types-with-errorresponse)

Define 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()));
}
```

### Using Error Types in TypeScript

[](#using-error-types-in-typescript)

```
import type { ErrorOf, ApiError } from '@/api';
import { api } from '@/lib/api';

// Get the error type for a specific route
type CreateUserError = ErrorOf;
// 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);
        }
    }
}
```

### Error Handling with React Query

[](#error-handling-with-react-query)

```
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();
            }
        },
    });

    // ...
}
```

Type Helpers
------------

[](#type-helpers)

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;

// Response type
type UserResponse = ResponseOf;

// URL parameters type
type UserParams = ParamsOf; // { user: number }

// Query parameters type
type UserQuery = QueryOf; // { page?: number, per_page?: number }

// Error type (custom or ValidationError)
type StoreUserError = ErrorOf;
```

React Query Integration
-----------------------

[](#react-query-integration)

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.)
],
```

### Generated Files

[](#generated-files-1)

FileDescription`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`)### Setup

[](#setup)

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';
```

### Resource-Based Queries (`queries.ts`)

[](#resource-based-queries-queriests)

The `queries.ts` file generates query factories for each API resource, providing:

- Pre-configured `queryOptions()` and `infiniteQueryOptions()` for each GET endpoint
- Type-safe query keys with `keys` object for cache management
- Automatic infinite query support for paginated endpoints

```
import { 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 }]
```

### Low-Level Utilities (`react-query.ts`)

[](#low-level-utilities-react-queryts)

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 }]
```

### Full Example

[](#full-example)

```
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 Loading...;

    return (

            {data?.name}
             updateUser.mutate({ name: 'New Name' })}>
                Update

    );
}
```

### Infinite Queries for Pagination

[](#infinite-queries-for-pagination)

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 (

            {data?.pages.flatMap(page => page.data).map(user => (
                {user.name}
            ))}
            {hasNextPage && (
                 fetchNextPage()}
                    disabled={isFetchingNextPage}
                >
                    {isFetchingNextPage ? 'Loading...' : 'Load More'}

            )}

    );
}
```

### Resource-Based Mutations (`mutations.ts`)

[](#resource-based-mutations-mutationsts)

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 (
         createUser.mutate({
                body: { name: 'John', email: 'john@example.com' }
            })}
            disabled={createUser.isPending}
        >
            Create User

    );
}

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 (
         deleteUser.mutate({ user: userId })}>
            Delete

    );
}
```

**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']
```

Inertia.js Integration
----------------------

[](#inertiajs-integration)

```
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 (

            Create

    );
}
```

API Client Configuration
------------------------

[](#api-client-configuration)

### Basic Setup

[](#basic-setup)

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' },
});
```

### Usage

[](#usage)

```
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' } });
```

### Per-Request Options

[](#per-request-options)

All API methods accept an optional `RequestOptions` object as the last parameter:

```
interface RequestOptions {
    headers?: Record;  // Custom headers for this request
    next?: NextCacheOptions;           // Next.js cache configuration
    mobile?: MobileOptions;            // Mobile/React Native options
    signal?: AbortSignal;              // Abort signal
}
```

### Next.js App Router

[](#nextjs-app-router)

#### Server Components

[](#server-components)

```
// 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 ;
}
```

#### Server Actions

[](#server-actions)

```
// 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}`);
}
```

#### Route Handlers

[](#route-handlers)

```
// 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);
}
```

### React Query Integration

[](#react-query-integration-1)

#### With Configured API (Recommended)

[](#with-configured-api-recommended)

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 Loading...;

    return (

            {user?.name}
             updateMutation.mutate({ name: 'New Name' })}>
                Update

    );
}
```

Command Options
---------------

[](#command-options)

```
# 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
```

### Command Options Reference

[](#command-options-reference)

OptionDescription`--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 clientCustomizing Stubs
-----------------

[](#customizing-stubs)

Publish the stub templates:

```
php artisan vendor:publish --tag=trpc-stubs
```

Templates will be copied to `resources/views/vendor/trpc/`.

Known Limitations
-----------------

[](#known-limitations)

- **Route model binding:** Parameters are typed as `number | string` by default. Custom types require explicit `#[TypedRoute]` configuration.
- **Union/polymorphic responses:** Not automatically detected. Use a single response Data class or document via `#[TypedRoute(response: ...)]`.
- **Middleware inference:** Auth detection relies on common middleware names (`auth`, `auth:*`). Custom auth middleware may not be detected.
- **Closure routes:** Routes without controller methods cannot have types extracted automatically.

Troubleshooting
---------------

[](#troubleshooting)

### "No API routes found"

[](#no-api-routes-found)

- Ensure your routes use the configured `api_prefix` (default: `api`)
- Check that routes are registered before running the command
- Try `php artisan route:list` to verify routes exist

### Generated types are `unknown`

[](#generated-types-are-unknown)

- Add `#[TypeScript]` attribute to your Data classes
- Run `php artisan typescript:transform` first
- Check that `laravel.d.ts` exists in your output directory

### TypeScript errors after regeneration

[](#typescript-errors-after-regeneration)

- Clear your TypeScript cache: `rm -rf node_modules/.cache`
- Ensure your `tsconfig.json` includes the output directory
- Check for circular dependencies in your Data classes

### Route names are duplicated

[](#route-names-are-duplicated)

- The generator appends `_1`, `_2` suffixes for duplicate names
- Use unique route names or configure `route_name_mappings` in config

Versioning
----------

[](#versioning)

This package follows [Semantic Versioning](https://semver.org/):

- **Patch (0.1.x):** Bug fixes, no changes to generated output shape
- **Minor (0.x.0):** New features, may add fields to generated output
- **Major (x.0.0):** Breaking changes to generated output structure or API

During the **0.x beta period**, minor versions may include breaking changes. These will be clearly documented in the [CHANGELOG](CHANGELOG.md).

Backstory
---------

[](#backstory)

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](https://github.com/oybek-daniyarov/laravel-trpc/issues). I'll fix it.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE.md) for details.

Credits
-------

[](#credits)

- [Oybek Daniyarov](https://github.com/oybek-daniyarov)
- [Claude](https://claude.ai) by Anthropic — the AI that wrote the code
- [Spatie](https://spatie.be) for laravel-data and typescript-transformer
- [Laravel](https://laravel.com) team for Wayfinder, which inspired some of the thinking here

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance83

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 87.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~7 days

Recently: every ~19 days

Total

12

Last Release

84d ago

PHP version history (2 changes)v0.1.0-betaPHP ^8.2

v0.1.4-betaPHP ^8.3

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/100561459?v=4)[Oybek](/maintainers/oybek-daniyarov)[@oybek-daniyarov](https://github.com/oybek-daniyarov)

---

Top Contributors

[![oybek-daniyarov](https://avatars.githubusercontent.com/u/100561459?v=4)](https://github.com/oybek-daniyarov "oybek-daniyarov (28 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (4 commits)")

---

Tags

apilaravelroutestypescriptinertiafull stackcodegentype-safePostmanreact-queryclient-generatortyped-api

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/oybek-daniyarov-laravel-trpc/health.svg)

```
[![Health](https://phpackages.com/badges/oybek-daniyarov-laravel-trpc/health.svg)](https://phpackages.com/packages/oybek-daniyarov-laravel-trpc)
```

###  Alternatives

[darkaonline/l5-swagger

OpenApi or Swagger integration to Laravel

3.0k37.6M134](/packages/darkaonline-l5-swagger)[knuckleswtf/scribe

Generate API documentation for humans from your Laravel codebase.✍

2.3k14.2M63](/packages/knuckleswtf-scribe)[api-platform/laravel

API Platform support for Laravel

58171.5k14](/packages/api-platform-laravel)[scriptdevelop/whatsapp-manager

Paquete para manejo de WhatsApp Business API en Laravel

783.8k](/packages/scriptdevelop-whatsapp-manager)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
