Skip to content
hm::dev
← all posts

Why Laravel is still my default for a client's internal CRUD app

updated 2026-08-01 · by Hagop Minassian

A client asked me for an internal tool last month: staff log deliveries, mark them fulfilled, export a monthly report. I quoted two days. I built it in Laravel instead of the Node and React stack I use for client-facing product work, and the two-day estimate held. That’s the part worth explaining, because on paper Node and React should have been the faster call. I know that stack better. I use it every week.

The scaffolding tax nobody counts

Hand-rolling the same tool in Node looks simple until you list what “simple” actually requires. Auth needs a library or a hand-built session flow. The database layer needs an ORM, and Prisma needs its own migration setup before it does anything useful. Then there’s the UI: a table component with sorting and pagination, a form component wired to validation, an edit view that matches the create view without duplicating the field list. None of this is hard. All of it is time, and most of it has nothing to do with the actual business logic the client is paying for.

Laravel skips most of that list by default. php artisan make:model Delivery -mcr gives you the model, a migration, a controller, and a resource route in one command. Eloquent relationships read like the business logic they describe. Laravel Breeze sets up auth, including password resets, in a single install command. None of it is exotic. It’s just already there.

What Filament actually saves

The bigger gap shows up once you add an admin panel. For this project I used Filament, a Laravel package that generates a full CRUD interface, list, create, edit, from a model definition. Define the Delivery model’s fields once, tell Filament which ones are searchable and which are required, and the list view, the form, the validation, and the pagination all come from that one definition.

Doing the equivalent in React means building a table view, wiring React Hook Form and a schema validator like Zod, and keeping the create and edit forms in sync by hand whenever a field changes. I’ve built that pattern more than once. It works, but it’s the same 200 lines of scaffolding every time, and Filament gives me the same result from a class that’s mostly annotations.

Where the gap disappears

This stops being true the moment the “internal tool” grows real product requirements. Heavy client-side interactivity, live updates, a mobile app sharing the same API, these all shrink Laravel’s speed advantage fast, because now you’re paying the cost of stitching two ecosystems together instead of the cost of building inside one. A team that’s already all-in on TypeScript also pays a real tax bringing PHP into the stack just for the admin side, even if the admin side ships faster on its own.

The pattern I’ve noticed: Laravel wins when the interface is the boring part and loses when the interface is the point.

The rule I actually use

Before I pick a stack for an internal tool, I count CRUD screens, not languages on the team’s resume. If the primary user is internal staff and the interface is mostly forms and tables, I reach for Laravel and Filament first. If the interface has to feel like a product someone pays for, I reach for React first and decide on the backend language second.

Next internal tool you scope, try counting the screens before you count the frameworks. It changes the estimate more than you’d expect.

Next
What changed when I swapped a Node script for Deno