In an existing project
LaraPack doesn't need a fresh project. It generates just the same inside an existing package or a running Laravel 13 application. What changes is where it writes and what you have to wire up yourself, and the type in composer.json decides it:
type in composer.json | Mode | Writes to |
|---|---|---|
library, or no type | Package | src/, under the package namespace |
Anything else, usually project | Application | app/, under App\ |
The full list of differences is in Package or application.
A freshly created application?
If the app is still what laravel new produced, skip this page: the base application does all of this and also sets up the site, authentication and admin panel.
In an existing package
1. Check composer.json
typeislibrary, or absent.autoload.psr-4has a namespace pointing atsrc/. LaraPack reads the namespace from there, and route names derive from it:Acme\Catalog\givesapi.acme.catalog.<model>.*.- There is no
versionkey. If there is, remove it: Composer discards every tag that doesn't match it.
2. Install LaraPack and what the generated code uses
composer require --dev innoboxrr/larapack-generator
composer require innoboxrr/search-surge:^3.0 innoboxrr/support:^2.1 innoboxrr/traits:^2.1 laravel/sanctum:^4.3 maatwebsite/excel:^4.0LaraPack goes in require-dev: it's the generator. The code it generates uses traits (models and metas), search-surge (indexes and filters), support (meta saving), Sanctum (auth:sanctum on routes) and Excel (exports), and those belong in require.
3. Providers and config, once
php vendor/bin/builder larapack:providers
php vendor/bin/builder larapack:configlarapack:providers creates the App, Auth, Event and Route providers and lists them in extra.laravel.providers, so any application installing the package discovers them. larapack:config creates the package config file. The importer doesn't generate these: without them neither routes nor events load.
4. Declare, validate and generate
php vendor/bin/builder larapack:schema
# write laraimport.json
php vendor/bin/builder larapack:validate --vue
php vendor/bin/builder larapack:import --vue --dry-run
php vendor/bin/builder larapack:import --vueSwap --vue for --react, or use both. The interface module goes into resources/vue or resources/react, with its own package.json.
If you already have files with those names
LaraPack doesn't overwrite what it didn't write: an existing file is skipped, and with --force a file the manifest doesn't know is still left alone. Always read the --dry-run output before generating over existing code.
If the package was generated with LaraPack 5.x, before the manifest existed, follow the Upgrade guide.
5. Align the package with the baseline
php vendor/bin/builder larapack:auditlarapack:audit compares the package against the versions that govern the whole ecosystem and against what it needs to release: real tests, tests.yml, release.yml. Each finding says what to fix.
To get workflows, VERSION, pint.json and phpstan.neon.dist identical to a new package's, generate a reference package outside yours and copy what you're missing:
php vendor/bin/builder larapack:new acme/reference ../reference--dry-run won't do here: it builds the package in a temporary directory and deletes it.
In an existing application
A running Laravel 13 application, without the base app. LaraPack generates the API and the interface module; wiring the interface into your application is up to you.
1. What the application must have
| What | How | Why |
|---|---|---|
| Sanctum | php artisan install:api, then confirm with composer show laravel/sanctum | Generated routes use auth:sanctum. install:api uses the PATH composer and fails silently. |
| Session on the API | $middleware->statefulApi() in bootstrap/app.php | The interface calls the API with the session cookie. Without it, every request answers 401. |
| Unwrapped responses | JsonResource::withoutWrapping() in AppServiceProvider::boot() | The table expects data, meta and links at the root. Without it the table is empty. |
| Named routes | innoboxrr/routes-to-json | The frontend requests every URL by name. |
A Notifiable user | Laravel's User already is | Exports notify the user. |
isAdmin() on the user | Optional | Each policy's before() lets administrators through. Without the method, nobody is one. |
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
})
// app/Providers/AppServiceProvider.php
use Illuminate\Http\Resources\Json\JsonResource;
public function boot(): void
{
JsonResource::withoutWrapping();
}If you want the same rule as the base app, an administrator is anyone whose email is in ADMIN_EMAILS:
// config/auth.php
'admins' => array_values(array_filter(array_map('trim', explode(',', (string) env('ADMIN_EMAILS', ''))))),
// app/Models/User.php
public function isAdmin(): bool
{
$admins = array_map('strtolower', (array) config('auth.admins', []));
return in_array(strtolower((string) $this->email), $admins, true);
}2. Install LaraPack and its dependencies
composer require --dev innoboxrr/larapack-generator
composer require innoboxrr/search-surge:^3.0 innoboxrr/support:^2.1 innoboxrr/traits:^2.1 innoboxrr/routes-to-json:^2.1 maatwebsite/excel:^4.03. Declare, validate and generate
Create laraimport.json at the application root and generate:
php artisan larapack:validate laraimport.json --vue
php artisan larapack:import laraimport.json --vue --dry-run
php artisan larapack:import laraimport.json --vuephp artisan larapack:validate laraimport.json --react
php artisan larapack:import laraimport.json --react --dry-run
php artisan larapack:import laraimport.json --reactIn an application:
- Code goes to
app/. The API forOrderLinelives inroutes/api/models/order_line.php, with URLapi/app/order_line/...and namesapi.app.order_line.*. - Factories go to
Database\Factoriesand tests toTests\Feature\Models. Tests extend yourtests/TestCase.phpand sign in using the factory of theauth.providers.users.modelmodel. - If a table already has a create migration LaraPack didn't write, such as Laravel's
users,larapack:importskips it and writes no alter migrations against it. If that table needs more columns, write the migration yourself.
4. Create and register the providers
php artisan larapack:route-service-provider
php artisan larapack:event-service-providerRegister them in bootstrap/providers.php. Laravel doesn't discover providers from an application's composer.json:
// bootstrap/providers.php
return [
App\Providers\AppServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
];Without the RouteServiceProvider no generated route exists. Without the EventServiceProvider, an export never notifies whoever requested it.
5. Migrate and export routes
Tell routes-to-json where to write. A relative path resolves from the project root:
JSON_ROUTES_FILE=resources/vue/routes.jsonphp artisan migrate
php artisan route:jsonEvery time you add an endpoint, run route:json again.
Exports work with no configuration: they store to the local disk and notify by email. To change that, php artisan larapack:config creates config/larapack.php with notification_via, export_disk and excel_view. It never writes to config/app.php.
6. Wire up the interface
In an application the module has no package.json or vite.config.js: your application's Vite builds it, so your package.json must declare what the module uses.
npm install axios vue vue-router@4 pinia@3 innoboxrr-form-core innoboxrr-form-elements innoboxrr-vue-datatable innoboxrr-http-request innoboxrr-i18n innoboxrr-js-validator innoboxrr-route-resolver
npm install --save-dev vite@8 @vitejs/plugin-vue@6 laravel-vite-pluginnpm install axios react react-dom react-router-dom@7 zustand@5 innoboxrr-form-core innoboxrr-react-form-elements innoboxrr-react-datatable innoboxrr-http-request innoboxrr-i18n innoboxrr-js-validator innoboxrr-route-resolver
npm install --save-dev vite@8 @vitejs/plugin-react@6 laravel-vite-pluginThen, in your interface's entry point: configure axios for the session, load routes and translations, and mount the module's routes under /admin. These examples assume the entry point lives in resources/js/:
import axios from 'axios'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
import { setRoutes } from 'innoboxrr-route-resolver'
import { addTranslations, setLocale } from 'innoboxrr-i18n'
import '../vue/src/theme.js'
import adminModule, { routes as moduleRoutes, translations } from '../vue/index.js'
import routes from '../vue/routes.json'
import App from './App.vue'
import AdminLayout from './AdminLayout.vue'
// The API is called with Sanctum's session cookie.
axios.defaults.withCredentials = true
axios.defaults.withXSRFToken = true
axios.defaults.headers.common.Accept = 'application/json'
setRoutes(routes)
addTranslations(translations)
setLocale(document.documentElement.lang)
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/admin', component: AdminLayout, children: moduleRoutes }],
})
// Module routes declare `meta.auth`; what happens without a session is the app's call.
// isLoggedIn() is however your app knows.
router.beforeEach((to) => (to.matched.some((record) => record.meta.auth) && ! isLoggedIn() ? '/login' : true))
createApp(App).use(createPinia()).use(router).use(adminModule).mount('#app')import axios from 'axios'
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'
import { setRoutes } from 'innoboxrr-route-resolver'
import { addTranslations, setLocale } from 'innoboxrr-i18n'
import { ConfirmHostComponent, ToastRegionComponent } from 'innoboxrr-react-form-elements'
import 'innoboxrr-react-form-elements/src/css/form-elements.css'
import '../react/src/theme.js'
import { registerModuleRoutes, routes as moduleRoutes, translations } from '../react/index.js'
import routes from '../react/routes.json'
axios.defaults.withCredentials = true
axios.defaults.withXSRFToken = true
axios.defaults.headers.common.Accept = 'application/json'
setRoutes(routes)
// Same prefix the routes are mounted under: the table builds its links by name.
registerModuleRoutes('/admin')
addTranslations(translations)
setLocale(document.documentElement.lang)
function AdminLayout() {
// Module routes declare `handle.auth`; what happens without a session is the app's call.
return (
<>
<Outlet />
<ToastRegionComponent />
<ConfirmHostComponent />
</>
)
}
const router = createBrowserRouter([
{ path: '/admin', element: <AdminLayout />, children: moduleRoutes },
])
createRoot(document.getElementById('app')).render(<RouterProvider router={router} />)In Vue, mount ToastRegionComponent and ConfirmHostComponent from innoboxrr-form-elements once in App.vue, next to <RouterView />.
Laravel must serve the SPA view on any /admin path. The view carries the application locale in the lang attribute of <html> (which setLocale reads), a <div id="app"> and the @vite directive for your entry point:
// routes/web.php
Route::view('/admin/{any?}', 'admin')->where('any', '.*')->middleware('auth');{{-- resources/views/admin.blade.php --}}
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@vite('resources/js/app.js')
</head>
<body>
<div id="app"></div>
</body>
</html>Your login works
If the application already signs users in through its own web login on the same domain, the interface uses that same session cookie. If the SPA handles login, request GET /sanctum/csrf-cookie before the login POST.
APP_URL
The address you open the app with must be in SANCTUM_STATEFUL_DOMAINS, which by default includes the APP_URL host and port. Otherwise the session works on pages but the API answers 401. See Requirements.
7. Check
php artisan larapack:verify
php artisan testCommit laraimport.json and .larapack/manifest.json together with the generated code.
Extending a project that already uses LaraPack
If the project already generates with LaraPack, adding a model, a column or a rule is the normal flow: edit laraimport.json, validate, import --dry-run, import --force, and review what the importer says it kept. See Regenerate without destroying.