Controllers | typescript-transformer | Spatie

 SPATIE

  TypeScript Transformer
=========================

spatie.be/open-source

  [Docs](https://spatie.be/docs)  [Typescript-transformer](https://spatie.be/docs/typescript-transformer/v3)  Laravel  Controllers

 Version   v3   v2   v1

 Other versions for crawler [v3](https://spatie.be/docs/typescript-transformer/v3) [v2](https://spatie.be/docs/typescript-transformer/v2) [v1](https://spatie.be/docs/typescript-transformer/v1)

  Controllers
- [ Introduction ](https://spatie.be/docs/typescript-transformer/v3/introduction)
- [ Postcardware ](https://spatie.be/docs/typescript-transformer/v3/postcardware)
- [ Installation ](https://spatie.be/docs/typescript-transformer/v3/installation)
- [ Questions &amp; issues ](https://spatie.be/docs/typescript-transformer/v3/questions-and-issues)
- [ Changelog ](https://spatie.be/docs/typescript-transformer/v3/changelog)
- [ About us ](https://spatie.be/docs/typescript-transformer/v3/about-us)

Getting started
---------------

- [ Setting up ](https://spatie.be/docs/typescript-transformer/v3/getting-started/setting-up)
- [ Running TypeScript Transformer for the first time ](https://spatie.be/docs/typescript-transformer/v3/getting-started/first-run)
- [ Special attributes ](https://spatie.be/docs/typescript-transformer/v3/getting-started/attributes)
- [ Typing properties ](https://spatie.be/docs/typescript-transformer/v3/getting-started/typing-properties)
- [ Replacing common types ](https://spatie.be/docs/typescript-transformer/v3/getting-started/replacing-types)
- [ Formatters ](https://spatie.be/docs/typescript-transformer/v3/getting-started/formatters)

Laravel
-------

- [ Installation and setup ](https://spatie.be/docs/typescript-transformer/v3/laravel/installation-and-setup)
- [ Laravel Data ](https://spatie.be/docs/typescript-transformer/v3/laravel/laravel-data)
- [ Controllers ](https://spatie.be/docs/typescript-transformer/v3/laravel/controllers)
- [ Routes ](https://spatie.be/docs/typescript-transformer/v3/laravel/routes)
- [ Route filters ](https://spatie.be/docs/typescript-transformer/v3/laravel/route-filters)
- [ Watch mode ](https://spatie.be/docs/typescript-transformer/v3/laravel/watch-mode)

Custom transformers
-------------------

- [ Getting started ](https://spatie.be/docs/typescript-transformer/v3/transformers/getting-started)
- [ Class transformer ](https://spatie.be/docs/typescript-transformer/v3/transformers/class-transformer)
- [ Enum transformer ](https://spatie.be/docs/typescript-transformer/v3/transformers/enum-transformer)

Transformed providers
---------------------

- [ Getting started ](https://spatie.be/docs/typescript-transformer/v3/providers/getting-started)
- [ Using different writers in providers ](https://spatie.be/docs/typescript-transformer/v3/providers/writers-in-providers)
- [ Logging in providers ](https://spatie.be/docs/typescript-transformer/v3/providers/logging)
- [ Referencing types ](https://spatie.be/docs/typescript-transformer/v3/providers/references)
- [ Helpers ](https://spatie.be/docs/typescript-transformer/v3/providers/helpers)

TypeScript nodes
----------------

- [ Introduction ](https://spatie.be/docs/typescript-transformer/v3/typescript-nodes/introduction)
- [ Building your own TypeScript node ](https://spatie.be/docs/typescript-transformer/v3/typescript-nodes/custom-nodes)
- [ Visiting TypeScript nodes ](https://spatie.be/docs/typescript-transformer/v3/typescript-nodes/visitor)
- [ Node reference ](https://spatie.be/docs/typescript-transformer/v3/typescript-nodes/reference)

Watch mode
----------

- [ How does it work? ](https://spatie.be/docs/typescript-transformer/v3/watch-mode/how-it-works)
- [ Setting up the runner ](https://spatie.be/docs/typescript-transformer/v3/watch-mode/setting-up-the-runner)
- [ Watch events ](https://spatie.be/docs/typescript-transformer/v3/watch-mode/watch-events)
- [ PHP Nodes ](https://spatie.be/docs/typescript-transformer/v3/watch-mode/php-nodes)

Advanced
--------

- [ Extensions ](https://spatie.be/docs/typescript-transformer/v3/advanced/extensions)
- [ Managing transformers ](https://spatie.be/docs/typescript-transformer/v3/advanced/managing-transformers)
- [ Loggers ](https://spatie.be/docs/typescript-transformer/v3/advanced/loggers)
- [ Custom writers ](https://spatie.be/docs/typescript-transformer/v3/advanced/custom-writers)

 Controllers
===========

###  On this page

1. [ Setup ](#content-setup)
2. [ How it works ](#content-how-it-works)
3. [ Invokable controllers ](#content-invokable-controllers)
4. [ Response type resolution ](#content-response-type-resolution)
5. [ Action name resolvers ](#content-action-name-resolvers)
6. [ Route filters ](#content-route-filters)
7. [ Output location ](#content-output-location)
8. [ Watch mode ](#content-watch-mode)

TypeScript Transformer can generate typed TypeScript objects for your Laravel controllers. Each controller action becomes a callable function that knows its URL, HTTP method and route parameters. Response and request types are automatically extracted from your controller method signatures.

This feature is still quite beta but we found it so cool we already wanted to share it. We would love to hear your feedback and ideas for improvement!

Setup
-----------------------------------------------------------------------

Add the `LaravelControllerTransformedProvider` to your `TypeScriptTransformerServiceProvider`:

```
use Spatie\LaravelTypeScriptTransformer\TransformedProviders\LaravelControllerTransformedProvider;

protected function configure(TypeScriptTransformerConfigFactory $config): void
{
    $config->provider(new LaravelControllerTransformedProvider());
}
```

The next time you run `php artisan typescript:transform`, a TypeScript file will be generated in the `controllers/` directory containing typed objects for all your controllers.

How it works
--------------------------------------------------------------------------------------------

Given a controller like this:

```
class PostsController
{
    public function index(): LengthAwarePaginator
    {
        // ...
    }

    public function show(string $post): PostData
    {
        // ...
    }

    public function store(PostData $data): PostData
    {
        // ...
    }
}
```

With routes:

```
Route::get('posts', [PostsController::class, 'index']);
Route::get('posts/{post}', [PostsController::class, 'show']);
Route::post('posts', [PostsController::class, 'store']);
```

The next time you run `php artisan typescript:transform`, a typed TypeScript object will be generated for this controller. Each action becomes a callable function that returns the URL and HTTP method:

```
import { PostsController } from './controllers';

const { url, method } = PostsController.index();
// { url: '/posts', method: 'get' }
```

Controllers in nested namespaces like `App\Http\Controllers\Admin\UsersController` are placed in subdirectories:

```
import { UsersController } from './controllers/Admin';
```

Actions with route parameters require them as the first argument:

```
const { url, method } = PostsController.show({ post: 1 });
// { url: '/posts/1', method: 'get' }
```

You can pass query parameters via the options argument:

```
const { url } = PostsController.index({ query: { page: 2, per_page: 15 } });
// '/posts?page=2&per_page=15'
```

When an action is registered for multiple HTTP methods, the default method (first registered) is used. You can access a specific variant directly:

```
PostsController.update({ post: 1 })
// { url: '/posts/1', method: 'put' }

PostsController.update.patch({ post: 1 })
// { url: '/posts/1', method: 'patch' }
```

### Request and response types

The generated namespace for each action contains `Request` and `Response` types. You can use these to type your frontend code:

```
import { PostsController } from './controllers';

async function createPost(data: PostsController.store.Request) {
    const { url, method } = PostsController.store();

    const response = await fetch(url, {
        method,
        body: JSON.stringify(data),
    });

    return await response.json() as PostsController.store.Response;
}
```

We'll talk more about how request and response types are determined later in this document.

Invokable controllers
-----------------------------------------------------------------------------------------------------------------------

Invokable controllers (with a single `__invoke` method) are generated as a single callable rather than an object with action methods:

```
class ShowDashboardController
{
    public function __invoke(): DashboardData
    {
        // ...
    }
}
```

You can call them directly without specifying an action name:

```
const { url, method } = ShowDashboardController();
// { url: '/dashboard', method: 'get' }
```

Request and response types are available on the controller namespace:

```
const response = await fetch(url);
const data = await response.json() as ShowDashboardController.Response;
```

Response type resolution
--------------------------------------------------------------------------------------------------------------------------------

TypeScript Transformer inspects your controller methods to determine response types. The following return types are recognized:

- **Scalar types**: `string`, `int`, `float`, `bool` and `null`
- **Data objects**: any class implementing `Spatie\LaravelData\Contracts\BaseData`
- **Arrays and shapes**: `array`, `array{name: string, age: int}`
- **Collections**: `Collection`
- **Data collections**: `DataCollection`, `PaginatedDataCollection`, `CursorPaginatedDataCollection`
- **Wrapped responses**: `Response` or `Inertia\Response` (the wrapper is unwrapped)

You can use these types as a PHP return type or in a PHPDoc annotation.

At the moment we're unable to detect what kind of Inertia response will be returned and we do not support Laravel's `Resource` classes, but we plan to add support for these in the future.

When a return type cannot be resolved to a known TypeScript type (e.g. a plain `Response` without a generic), the response type will be `object`.

### Request type resolution

Request types are detected by looking for a method parameter that is a Data object(from spatie/laravel-data). The first matching parameter becomes the `Request` type:

```
public function store(StorePostData $data): PostData
{
    // ...
}
```

If no Data object parameter is found, the request type will be `object`.

In the future we plan to add support for detecting Laravel's `FormRequest` classes as well.

Action name resolvers
-----------------------------------------------------------------------------------------------------------------------

The generated TypeScript file path is derived from the controller's fully qualified class name. For example, `App\Http\Controllers\Posts\PostsController` results in `App/Http/Controllers/Posts/PostsController.ts`.

Since most Laravel controllers live under `App\Http\Controllers`, you'll probably want to strip that prefix. Use the `StrippedActionNameResolver` to do this:

```
use Spatie\LaravelTypeScriptTransformer\ActionNameResolvers\StrippedActionNameResolver;

$config->provider(new LaravelControllerTransformedProvider(
    actionNameResolver: new StrippedActionNameResolver([
        'App\Http\Controllers' => null,
    ]),
));
```

This turns `App\Http\Controllers\Posts\PostsController` into `Posts/PostsController.ts`. Setting the replacement to `null` strips the prefix entirely. You can also provide a replacement string:

```
new StrippedActionNameResolver([
    'App\Http\Controllers' => 'controllers',
])
// App\Http\Controllers\Posts\PostsController → controllers/Posts/PostsController
```

### Custom resolver

For full control, use the `ClosureActionNameResolver`. It receives the controller class name and should return an array of path segments:

```
use Spatie\LaravelTypeScriptTransformer\ActionNameResolvers\ClosureActionNameResolver;

$config->provider(new LaravelControllerTransformedProvider(
    actionNameResolver: new ClosureActionNameResolver(
        fn (string $controllerClass) => ['api', class_basename($controllerClass)]
    ),
));
```

Route filters
-----------------------------------------------------------------------------------------------

You can exclude certain routes from controller generation using [route filters](/docs/typescript-transformer/v3/laravel/route-filters).

Output location
-----------------------------------------------------------------------------------------------------

By default, controller files are written to a `controllers/` directory inside your configured output directory. You can change this:

```
$config->provider(new LaravelControllerTransformedProvider(
    location: 'controllerDefinitions',
));
```

Watch mode
--------------------------------------------------------------------------------------

When running in [watch mode](/docs/typescript-transformer/v3/laravel/watch-mode), the controller provider watches your route directories (`routes/`, `bootstrap/` and `app/Providers/`) for changes to route definitions. You can customize which directories are watched:

```
$config->provider(new LaravelControllerTransformedProvider(
    routeDirectories: [
        base_path('routes'),
    ],
));
```

If you also want to regenerate controllers when a controller file changes, add the controller directory to your [watch directories](/docs/typescript-transformer/v3/laravel/watch-mode) configuration.

 A good
match?
-------------

### What we do best

- All things Laravel
- Custom frontend components
- Building APIs
- AI-powered features
- Simplifying things
- Clean solutions
- Integrating services

### Not our cup of tea

- WordPress themes
- Cutting corners
- Free mockups to win a job
- "Just execute the briefing"

 In short: we'd like to be a **substantial part** of your project.

 [ Get in touch via email ](mailto:info@spatie.be?subject=A%20good%20match%21&body=Tell%20us%20as%20much%20as%20you%20can%20about%0A-%20your%20online%20project%0A-%20your%20planning%0A-%20your%20budget%0A-%20%E2%80%A6%0A%0AAnything%20that%20helps%20us%20to%20start%20straightforward%21)
