PHPackages                             stubbedev/laravel-stoli - 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. stubbedev/laravel-stoli

ActiveLibrary

stubbedev/laravel-stoli
=======================

An enhanced Laravel extension to export named routes to JavaScript, allowing you to use route names instead of URLs. Supports route grouping for modular applications. Provides type hinting for route params and responses.

02—0%PHPCI passing

Since Mar 21Pushed 1mo agoCompare

[ Source](https://github.com/stubbedev/laravel-stoli)[ Packagist](https://packagist.org/packages/stubbedev/laravel-stoli)[ RSS](/packages/stubbedev-laravel-stoli/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (2)DependenciesVersions (1)Used By (0)

Laravel Stoli
=============

[](#laravel-stoli)

[![Software License](https://camo.githubusercontent.com/55c0218c8f8009f06ad4ddae837ddd05301481fcf0dff8e0ed9dadda8780713e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d627269676874677265656e2e7376673f7374796c653d666c61742d737175617265)](license)

`stubbedev/laravel-stoli` is a Laravel package that exports your application's named routes to TypeScript, enabling you to use route names instead of hardcoded URLs in your frontend code. It builds on top of [`spatie/laravel-data`](https://github.com/spatie/laravel-data) and [`spatie/laravel-typescript-transformer`](https://github.com/spatie/laravel-typescript-transformer) — both are required dependencies — and generates fully typed TypeScript definitions including parameter types inferred from FormRequest validation rules, URI constraint types, and response types derived from Data classes and the transformer's generated output.

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

[](#requirements)

- PHP 8.2+
- Laravel 11.15+
- `spatie/laravel-data` ^3|^4
- `spatie/laravel-typescript-transformer` ^3

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

[](#installation)

```
composer require stubbedev/laravel-stoli
```

Publish and configure `spatie/laravel-typescript-transformer` first — Stoli uses its output directory as the destination for all generated files:

```
php artisan vendor:publish --provider="Spatie\LaravelTypeScriptTransformer\TypeScriptTransformerServiceProvider"
```

Then publish the Stoli configuration:

```
php artisan vendor:publish --tag='stoli'
```

Generate routes and types:

```
php artisan typescript:transform   # generates types from Data classes
php artisan stoli:generate         # generates route files referencing those types
```

`stoli:generate` writes the following into the typescript-transformer output directory:

- `stoli.js` — the `RouteService` runtime
- `stoli.d.ts` — TypeScript declarations
- `api.ts` (or one file per module) — typed route definitions

Usage
-----

[](#usage)

### Basic

[](#basic)

```
import { RouteService } from "./stoli";
import routes from "./api";

const api = new RouteService({ routes });

api.generateFullURL("store.products.list");
// => https://example.com/api/store/products
```

### Typed parameters

[](#typed-parameters)

The generated route file exports `ApiRouteParams` and `ApiRouteName`. URI parameters (`{id}`, `{slug?}`) are always included. When a controller method accepts a `FormRequest`, its validation rules are also included as typed fields.

```
import routes, { type ApiRouteParams, type ApiRouteName } from "./api";
import { RouteService } from "./stoli";

const api = new RouteService({ routes });

// Route name autocompletion + typed params
api.generateFullURL("admin.products.update", { id: 42, name: "Notebook" });
//                                             ^^              ^^^^^^^^^^^
//                   required URI param (string | number)   from FormRequest
```

#### URI constraint types

[](#uri-constraint-types)

When routes declare `->where()` constraints, the parameter type is narrowed accordingly:

```
Route::get('/users/{id}', ...)->whereNumber('id');           // id: number
Route::get('/posts/{slug}', ...)->whereAlpha('slug');         // slug: string
Route::get('/items/{type}', ...)->whereIn('type', ['a','b']); // type: 'a' | 'b'
```

Without a constraint the type is `string | number`. Optional parameters (`{param?}`) become `param?: type`.

#### FormRequest parameter types

[](#formrequest-parameter-types)

Validation rules are reflected directly into TypeScript:

Laravel ruleTypeScript type`string`, `email`, `url`, `uuid`, …`string``integer`, `numeric`, `decimal:…``number``boolean`, `accepted`, `declined``boolean``array``Record``list`, `distinct``unknown[]``file`, `image``File``in:a,b,c` / `Rule::enum(MyEnum::class)``'a' | 'b' | 'c'``nullable` modifieradds `| null`Nested dot-notation (`address.city`)inline object typeWildcard arrays (`tags.*`)`string[]` / `{ … }[]`### Typed responses

[](#typed-responses)

Each generated module also exports `ApiRouteResponse`, a per-route map of response shapes. A route is included in the interface when its controller method has a return type annotation that is a `Spatie\LaravelData\Data` subclass and that class has been transformed by `php artisan typescript:transform`.

The generated interface looks like:

```
import type { UserData, UserResource } from '../types/generated';

export interface ApiRouteResponse {
    'api.users.show': UserData;
    'api.users.index': UserResource;
}
```

Routes without a resolvable Data return type are absent from the interface. The axios router (see below) falls back to `Record` for those routes.

#### Spatie Laravel Data

[](#spatie-laravel-data)

Controllers returning `Spatie\LaravelData\Data` objects are detected automatically. The TypeScript shape is derived from the class's public typed properties:

```
class UserData extends Data
{
    public function __construct(
        public int $id,
        public string $name,
        public ?string $bio,
        public AddressData $address,

        #[DataCollectionOf(TagData::class)]
        public DataCollection $tags,
    ) {}
}
```

Generates:

```
'api.users.show': { id: number; name: string; bio: string | null; address: { street: string; city: string }; tags: { id: number; label: string }[] }
```

Supported property types:

PHP typeTypeScript type`int`, `float``number``string``string``bool``boolean``?type` / `type|null``type | null`Nested `Data` subclassinline object type`DataCollection` + `#[DataCollectionOf(T::class)]``T[]``array` + `#[DataCollectionOf(T::class)]``T[]``DateTimeInterface``string``Collection``unknown[]`#### Spatie Laravel TypeScript Transformer

[](#spatie-laravel-typescript-transformer)

When `php artisan typescript:transform` has already been run, Stoli uses the generated type names directly instead of re-deriving inline shapes:

```
// Without typescript:transform (inline shape):
'api.users.show': { id: number; name: string; email: string | null };

// With typescript:transform (type reference + import):
import type { UserData } from '../types/generated';
// ...
'api.users.show': UserData;
```

The import path is computed relative to the module's output directory. Always run `typescript:transform` before `stoli:generate`:

```
php artisan typescript:transform
php artisan stoli:generate
```

### Axios router (optional)

[](#axios-router-optional)

Enable in `config/stoli.php`:

```
'axios' => true,
```

The generated router (`api.router.ts`) wraps axios with full type inference for both params and responses:

```
import { Stoli } from "./api.router";

// params typed from ApiRouteParams, response typed from ApiRouteResponse
const response = await Stoli.get("api.users.show", { id: 1 });
response.data; // typed as { data: { id: number; name: string; email: string } }

// routes without a detected response fall back to Record
const list = await Stoli.get("api.products.index");
list.data; // typed as Record
```

Requires axios: `npm install axios`

### Route service methods

[](#route-service-methods)

MethodDescription`generateFullURL(name, params?)`Full URL; leftover params are appended as query string`createURLWithoutQuery(name, params?)`URL with only URI params substituted; no query string`has(name)`Returns `true` if the route exists```
api.generateFullURL("admin.products.show", { id: "abc-123", page: 2 });
// => https://example.com/api/admin/products/abc-123?page=2

api.createURLWithoutQuery("admin.products.show", { id: "abc-123", page: 2 });
// => https://example.com/api/admin/products/abc-123
```

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

[](#configuration)

Publish and edit `config/stoli.php` to customise.

```
return [
    'split'  => true,        // true = one file per module, false = single file
    'axios'  => false,       // generate axios router wrapper
    'single' => [            // used when split = false
        'name' => 'api',     // output filename (without extension)
    ],
    'modules' => [
        [
            'match'       => '*',                         // route prefix filter (* = all)
            'name'        => 'api',                       // output file name (api.ts)
            'rootUrl'     => env('APP_URL', 'http://localhost'),
            'absolute'    => true,
            'prefix'      => null,
            'path'        => null,                        // defaults to typescript-transformer output dir
            'stripPrefix' => null,
        ],
    ],
];
```

All generated files are written to the `output_path` configured in `config/typescript-transformer.php`. Per-module `path` can override this for individual modules.

### Module options

[](#module-options)

OptionDefaultDescription`match``*`URL prefix to filter routes. `*` matches all, `/api/store` matches only routes under that path`name`—Output filename (without extension)`rootUrl``APP_URL`Base URL for absolute URLs`absolute``true`Generate absolute (`https://…`) or relative (`/…`) URLs`prefix``null`Prefix prepended to every generated URL`path`transformer output dirOutput directory for this module's `.ts` file`stripPrefix``null`Route name prefix to strip (e.g. `store.` turns `store.products.list` into `products.list`)### Multiple modules

[](#multiple-modules)

Split routes into separate typed files per API consumer:

```
'modules' => [
    [
        'match'   => '/api/store',
        'name'    => 'store',
        'rootUrl' => 'https://store.example.com',
    ],
    [
        'match'   => '/api/admin',
        'name'    => 'admin',
        'rootUrl' => 'https://admin.example.com',
    ],
],
```

This generates `store.ts` and `admin.ts`, each with their own `StoreRouteParams` / `AdminRouteParams` / `StoreRouteResponse` / `AdminRouteResponse` interfaces.

License
-------

[](#license)

MIT. See [`license`](./license).

###  Health Score

20

—

LowBetter than 14% of packages

Maintenance60

Regular maintenance activity

Popularity3

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% 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.

### Community

Maintainers

![](https://www.gravatar.com/avatar/eb1aabbff024e02005b49c6c3f0efbce381ec0b24f9636e9a326cb1d599f6335?d=identicon)[stubbedev](/maintainers/stubbedev)

---

Top Contributors

[![stubbedev](https://avatars.githubusercontent.com/u/89036433?v=4)](https://github.com/stubbedev "stubbedev (16 commits)")

### Embed Badge

![Health badge](/badges/stubbedev-laravel-stoli/health.svg)

```
[![Health](https://phpackages.com/badges/stubbedev-laravel-stoli/health.svg)](https://phpackages.com/packages/stubbedev-laravel-stoli)
```

PHPackages © 2026

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