DEV Community

Cover image for A complete Laravel API with clean architecture in 30 seconds, tests included
Loic Aron Mbassi Ewolo
Loic Aron Mbassi Ewolo

Posted on

A complete Laravel API with clean architecture in 30 seconds, tests included

I counted the files I create every time I add a resource to a Laravel API. Model, migration, controller, a form request, a resource, a factory, a seeder, a policy. If the project cares about architecture, add a DTO and a service class. Then the tests.

Twelve files before the first line of actual business logic. I got tired of typing them, so I wrote a generator. It's MIT, I maintain it solo, and v3.6 shipped this week.

One command

composer require --dev nameless/laravel-api-generator

php artisan make:fullapi Post --fields="title:string,content:text,status:enum(draft,published),published_at:datetime"
Enter fullscreen mode Exit fullscreen mode

Output:

app/
├── DTO/PostDTO.php
├── Enums/Status.php
├── Http/
│   ├── Controllers/PostController.php
│   ├── Requests/PostRequest.php
│   └── Resources/PostResource.php
├── Models/Post.php
├── Policies/PostPolicy.php
└── Services/PostService.php
database/
├── factories/PostFactory.php
├── migrations/xxxx_create_posts_table.php
└── seeders/PostSeeder.php
tests/
├── Feature/PostControllerTest.php
└── Unit/PostServiceTest.php
routes/api.php  ← apiResource registered
Enter fullscreen mode Exit fullscreen mode

[GIF demo.gif (voir fichier 12) : vraie exécution en terminal, pas Claude Design]

The VS Code extension: same engine, no terminal needed

[VISUEL 11 (fichier 13) : VRAI screenshot de l'extension VS Code, habillé ensuite dans Claude Design]

If you'd rather click than type, the free VS Code extension drives the same make:fullapi command, so the files are identical. You describe the entity in a form (fields, enums, relations, options) and a live preview shows the exact code before anything is written. Or you skip the form and import from your existing database, a YAML schema, a Mermaid diagram, or an OpenAPI/Swagger spec.

The button I always demo first is Open API Docs: it checks that Scramble is installed (and offers to install it if not), finds a running Laravel server or starts php artisan serve itself, detects the port, and opens the interactive docs of your new API in the browser. Migrations and seeding are one-click too, and if .env is missing the extension offers to create it from .env.example. You can go from an empty Laravel project to a browsable, documented, tested API without opening a terminal once.

scramble image

The part I actually care about: the architecture

Plenty of tools can spit out a model and a migration. What I wanted was the layering I end up building by hand on every serious project. The generated controller is thin:

public function store(PostRequest $request)
{
    $dto = PostDTO::fromRequest($request);
    $post = $this->service->create($dto);

    return new PostResource($post);
}
Enter fullscreen mode Exit fullscreen mode

Validation lives in the form request, business logic in PostService, and data crosses layers as a typed readonly PostDTO. When the project grows, the place where new logic should go already exists. That's the whole point.

Models your IDE can read

Each generated model comes with a real PHPDoc block:

/**
 * @property int $id
 * @property string $title
 * @property string $content
 * @property Status $status
 * @property \Illuminate\Support\Carbon|null $published_at
 * @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
 */
class Post extends Model
Enter fullscreen mode Exit fullscreen mode

So $post->title autocompletes in VS Code and PhpStorm without installing ide-helper. The generator already knows every field and relation at generation time; writing the docblock costs it nothing.

What enum(draft,published) produced

That one field definition created five coherent pieces:

  • App\Enums\Status, a native backed enum
  • the cast on the model
  • Rule::enum(Status::class) in both form requests
  • fake()->randomElement(Status::cases()) in the factory
  • $table->enum('status', [...]) in the migration

Before v3.6 the package just mapped enums to strings. Wiring the whole chain took longer than I expected, mostly because of the factory.

The tests are written, not scaffolded

This was my line in the sand. Most generators leave you empty test classes. Here php artisan test is green right after generation: index, store, validation errors, update, delete are all covered with real assertions.

it('creates a post', function () {
    $payload = Post::factory()->raw();

    $this->postJson('/api/posts', $payload)
        ->assertCreated();

    $this->assertDatabaseHas('posts', ['title' => $payload['title']]);
});
Enter fullscreen mode Exit fullscreen mode

That's the Pest style, from the --pest flag. Without it you get PHPUnit classes with the same coverage.

Docs, Postman, auth

--postman writes a ready-to-import collection at the project root. The controllers are written so Scramble can build OpenAPI docs from them without annotations. --auth scaffolds Sanctum (register, login, logout, middleware on your resources).

There is also a --from-database mode that reverse-engineers an existing database into full APIs, but that deserves its own article.

Zero lock-in

The package installs with --dev and the generated code has no dependency on it. No base classes, no runtime helpers, nothing to use. You can remove the generator after generating and the app doesn't notice. If you don't like the generated style, publish the 24 stubs (php artisan vendor:publish --tag=api-generator-stubs) and edit whichever ones you want.

Where it stops

It scaffolds a clean REST baseline and then it's out of the picture. Your domain logic, your weird queries, your edge cases: still your job. The difference is that you write them inside a service layer that already exists, with baseline tests already passing.

Links

composer require --dev nameless/laravel-api-generator
php artisan make:fullapi Product --fields="name:string,price:decimal,stock:integer" --pest
php artisan test
Enter fullscreen mode Exit fullscreen mode

If you try it and something breaks, open an issue. Schema edge cases reported by users have driven most of the recent releases, and I'd rather hear about yours than guess.

Top comments (0)