Getting Started

Build a Laravel Module Step by Step with Lara Dashboard

By Lara Dashboard 1 view
Build a Laravel Module Step by Step with Lara Dashboard
You can build a working Lara Dashboard module in an afternoon. Scaffold it with one Artisan command, generate a full CRUD screen with a second, check the permissions and menu it wired up, write a test, and package it as a ZIP that installs on any Lara Dashboard site.
Disclosure: Lara Dashboard is our open-source, MIT-licensed Laravel CMS and admin. This is our own tutorial. Every command and file name below comes from the repo's module development guide and the generator code in app/Console/Commands, as of November 2026 (v1.5.0, on Laravel 13, Livewire 4 and PHP 8.3+).

Quick answer: two commands give you a module with a real admin screen

The short version, for a module called Directory that manages business listings:
php artisan module:make Directory
php artisan module:make-crud Directory --model=Listing --fields="title:string,summary:text,logo:media,website:string,status:select:Draft|Published,is_featured:toggle"
The first command creates the module skeleton, including a sidebar menu entry and an empty admin dashboard page. The second creates the model, migration, controller, service, form request, Livewire datatable, views, policy and permissions for listings, and adds the routes and a menu item. Then you enable the module, review what was generated, and make it yours.
If you want the background first, read building modular Laravel applications with Lara Dashboard. This post is the hands-on follow-up.

Step 1: pick a module name the installer will accept

Pick a short, single-word name. The module installer only accepts folder names that start with a letter and use letters, digits and underscores. That rule lives in isValidModuleFolderName() in app/Services/Modules/ModuleService.php. It exists to stop path tricks in uploaded ZIPs, so do not fight it. Directory works. business-directory does not.
Modules sit on top of nwidart/laravel-modules, and the namespace follows the folder: a module in modules/Directory uses Modules\Directory. If you have used that package before, nothing here will surprise you.

Step 2: scaffold the module, and you already get a menu and a page

Run php artisan module:make Directory. Lara Dashboard ships its own stubs for this command, so the result is more than an empty Laravel package. Here is what matters in the generated folder:
module.json. Metadata: name, title, icon, version, author, category and pricing, which defaults to free. The admin module list reads this file.
description.md. A Markdown description. The guide marks it as required for marketplace submission, so fill it in now while you remember what the module does.
The service provider. It registers config, views, translations and commands, plus route and Livewire providers. After the app boots, it calls ModuleService::bootstrap().
app/Services/ModuleService.php. This is where the module plugs into the core. It adds your menu with the AdminFilterHook::ADMIN_MENU_GROUPS_BEFORE_SORTING filter and your permission groups with PermissionFilterHook::PERMISSION_GROUPS.
app/Services/MenuService.php. Builds the sidebar item as an AdminMenuItem with a label, an Iconify icon, a route, a priority and a permissions list.
routes/web.php. A route group under admin/directory, named admin.directory., behind the auth and verified middleware. It points to a Livewire dashboard page.
Assets. A vite.config.js plus CSS and JS entry points, so the module can ship its own compiled styles.
Enable it with php artisan module:enable Directory. You will see a Directory item in the sidebar that opens an empty dashboard page. That is your module running inside the admin, before you have written a line of code.

Step 3: generate a CRUD screen with module:make-crud

Next, add the thing the module manages. The generator needs either --model or --migration. With --model and --fields, it writes the migration for you. The field types it understands include string, text, editor, media, file, select with pipe-separated options, and toggle.
Run the command from the quick answer. In one pass it generates:
  • A Listing model and a migration for a directory_listings table
  • A Livewire datatable for the list view
  • A resource controller and a service class for the business logic
  • A form request for validation
  • Index, create, edit and show views with a shared form partial
  • A policy, registered in the module's service provider
  • A permission migration for view, create, edit and delete
  • Resource routes and a Listings item in the module menu
It also tries to run the module's migrations. Clear caches with php artisan optimize:clear and open the listings page. The command prints the URL when it can resolve the route.
The generated code follows the same pattern as the core: thin controllers, logic in a service, validation in a form request. We compared this approach with other generators in CRUD generators for Laravel admin panels.

Step 4: check the permissions before anyone else logs in

Generated code is a starting point. The permission setup is the part you should read line by line.
The permission migration calls PermissionService::syncPermissionsForRoles() with a group named after the model and four permissions: listing.view, listing.create, listing.edit and listing.delete. Its down() method removes them again. Every controller action calls $this->authorize() through the generated policy, so a user without the right permission gets a 403, not a blank page.
Three things to check by hand:
Who gets the permissions. Open Roles in the admin and decide which roles should have each one. An editor might view and edit listings but never delete them.
The menu. Add the view permission to the permissions array of the menu item, so people who cannot open the page do not see the link.
The policy. If listings belong to users, add an ownership check to update and delete. The generator cannot guess that rule.
Our role-based access control guide covers roles, policies and gates in more depth.

Step 5: make the generated code yours

Now the real work. The generator's own "next steps" list is a good order to follow:
  1. Review the model's fillable fields and casts. Cast is_featured to boolean.
  2. Tighten the form request rules. A website should be a valid URL, a title should have a max length.
  3. Adjust the datatable columns. Show title, status and featured, hide the long summary.
  4. Polish the form partial. Put the logo picker and the status select where editors expect them.
  5. Compile the module's CSS so the pages are styled without the Vite dev server running.
The last step is one command: php artisan module:compile-css Directory. Add --watch while you work. The guide asks you to use a Tailwind prefix for module styles, for example @import "tailwindcss" prefix(directory);, so your classes never clash with the core's.
Business rules go in the service class, not the controller or the Livewire component. If marking a listing as featured should also clear a cache or notify someone, the service is the one place to do it, and the one place to test it.
When other code should react to your module, use the hook system rather than editing core files. Hook::doAction() fires an action from your service, and another module can listen with Hook::addAction(). The full list of core hooks is in the repo's hooks documentation.

Step 6: test the module like any Laravel app

Module tests live in the module. Generate one with php artisan module:make-test ListingControllerTest Directory. The core phpunit.xml already includes modules/*/tests/Feature and modules/*/tests/Unit, so your tests run with the rest of the suite. The test suite uses Pest.
Start with the tests that catch the expensive mistakes:
  • A guest is redirected to login from the listings page
  • A user without listing.create gets a 403 on the create page
  • A user with the permission can create a listing and sees it in the table
  • Invalid input, such as a bad URL, fails validation with a clear error
  • Deleting a listing as a user without listing.delete is refused
If you do not have a staging and CI loop yet, set one up before the module reaches a client site. Our post on staging, tests and CI for Laravel CMS work shows a simple setup.

Step 7: package it as a ZIP that installs without npm

A module you can only run from your own repo is half done. Lara Dashboard can package it into a ZIP with pre-compiled assets, so the target server never needs Node.
php artisan module:package Directory --compile
That builds the assets into the module's dist/ folder and creates the ZIP. Add --minify for smaller CSS, --no-vendor to leave out the module's Composer packages, or --output= to choose the path. There is also module:zip, which runs Composer and npm installs before compiling, for when you build from a clean checkout.
On the target site, an admin uploads the ZIP under Modules, and the assets are published automatically. From the command line, unzip it into modules/, then run module:install-deps, module:migrate and module:enable.
A word on security: installing a module means running its PHP on your server. Since v1.5.0, the core hardens module uploads, as we covered in the v1.5.0 release notes. That still does not make an unknown module safe. Only install modules from people you trust, and read the code of anything that touches money or personal data.

Trade-offs: when a module is the wrong choice

Modules are our recommended way to add features. They are not always the right tool.
  • A one-off tweak to an existing screen is often better as a hook or filter in an existing module.
  • Code that every part of the app depends on, like a shared value object, may belong in a normal Composer package.
  • If your feature is mostly content, such as an FAQ page, LaraBuilder pages and posts may be enough with no code at all.
  • If your team does not want to run a Vite build, keep module UI to the core Blade components and classes.
  • If your module reaches into another module's tables, the boundary is wrong. Talk through hooks or events instead.
  • If you only need a quick CRUD admin over existing tables and no CMS, a plain admin kit may be lighter.
We explain the bigger picture, and where plugin-style architecture goes wrong, in modular Laravel apps vs WordPress plugin spaghetti.

Self-check before you ship your first module

  1. The folder name uses only letters, digits and underscores.
  2. module.json has the right title, version, author, icon and pricing.
  3. description.md explains what the module does and what it needs.
  4. Every route sits behind auth, and every action is authorized by a policy.
  5. Permissions are assigned to roles on purpose, not by default.
  6. The menu item is hidden from users who cannot open it.
  7. Validation rules are tightened beyond the generated defaults.
  8. Module CSS uses a Tailwind prefix and is compiled.
  9. Feature tests cover guest, forbidden and allowed paths.
  10. The packaged ZIP installs and enables on a clean test site.

Verdict

Building a Lara Dashboard module feels like building a small Laravel app, because it is one. The two generators take care of the boring wiring: menu, routes, policy, permissions, datatable and views. You spend your time on validation, business rules and tests, which is where the time should go.
The generated code is a first draft, not a finished product. Read the permissions, tighten the rules, and test the forbidden paths. If you want to try it, install Lara Dashboard, build the Directory module from this post, and then replace listings with the thing your client actually needs.

FAQ

Do I need to know nwidart/laravel-modules to build a Lara Dashboard module?

No, but it helps. Lara Dashboard uses that package for loading and enabling modules, and adds its own stubs and commands on top. If you know its basics, you already know most of the structure.

Can a module have its own Composer dependencies?

Yes. Each module can have its own composer.json and vendor folder. Install them with php artisan module:install-deps, or add a package with php artisan module:composer Directory require vendor/package.

What does module:make-crud need as input?

Either a model name with --model or an existing migration with --migration. With --fields, it also writes the migration. Without fields or a migration, it asks you to define fields interactively.

Where do module permissions come from?

The CRUD generator writes a migration that syncs view, create, edit and delete permissions for the model. You can also add permission groups in ModuleService with the permission groups filter. Either way, you still assign them to roles.

How do I install a finished module on another site?

Package it with php artisan module:package, then upload the ZIP in the admin under Modules. Assets are published automatically. From the command line, unzip it into modules/ and run install-deps, migrate and enable.

Can I sell my module?

Modules can declare free, paid or both pricing in module.json, and paid modules show a license activation option. We will cover selling through the marketplace in a separate post.
Laravel CMS Module Architecture laravel roles permissions laravel admin panel

Try Lara Dashboard for Free

Explore every feature live — no sign-up required.

Launch Live Demo