Build a Kitchen Orders API

This walkthrough teaches the main Assegai workflow from a blank project to a working feature.

By the end, you will have:

  • a running Assegai project
  • an orders feature generated by the CLI
  • DTOs for create and update input
  • a working in-memory provider implementation
  • a browsable /docs contract for the feature

You do not need to know every part of Assegai first. The tutorial introduces the pieces as they become useful.

What you are building

Imagine a small restaurant kitchen screen.

The kitchen needs a simple API to:

  • list orders
  • create a new order
  • update an existing order
  • remove an order

That makes it a good tutorial app because it uses the workflow Assegai is best at:

  • scaffold a feature
  • keep controller code thin
  • keep state and behavior in a provider
  • let DTOs shape input

Step 1: Create the project

Create a new project:

assegai new kitchen-orders-api
cd kitchen-orders-api

Then start the server:

assegai serve

Open the app in the browser. At this point, the goal is simply to confirm that the project boots.

Step 2: Generate the first feature

Now generate an orders resource:

assegai g r orders

This command creates a feature module with the standard parts already wired together.

Depending on the current generator template, you should see a structure close to this:

src/Orders/
├── DTOs/
│   ├── CreateOrderDTO.php
│   └── UpdateOrderDTO.php
├── Entities/
│   └── OrderEntity.php
├── OrdersController.php
├── OrdersModule.php
└── OrdersService.php

This is one of Assegai's core ideas:

  • the CLI does the repetitive structural work
  • you fill in the feature-specific behavior

Step 3: Inspect the generated API surface

If your project is configured to export OpenAPI on demand, generate the latest spec:

assegai api:export openapi

Then open:

http://localhost:5000/docs

Even before the feature is finished, this is useful because it lets you inspect the shape of the endpoints the CLI scaffold created.

Step 4: Shape the DTOs

The generated DTOs are the place where request input becomes explicit.

Open CreateOrderDTO.php and make it describe the data needed to create a kitchen order:

<?php

namespace Assegaiphp\KitchenOrdersApi\Orders\DTOs;

use Assegai\Validation\Attributes\IsArray;
use Assegai\Validation\Attributes\IsNotEmpty;
use Assegai\Validation\Attributes\IsString;

class CreateOrderDTO
{
  #[IsString]
  #[IsNotEmpty]
  public string $ticketNumber;

  #[IsString]
  #[IsNotEmpty]
  public string $status;

  #[IsArray]
  public array $items = [];
}

Then give UpdateOrderDTO.php the same fields, but keep in mind that update DTOs often allow partial input.

The important lesson here is not just validation.

It is that a DTO gives the request body a clear shape that your controller and provider can rely on.

When this feature eventually moves to the ORM, that same DTO can usually be passed straight into repository->create($dto) or repository->update(..., $dto) because it is already a plain PHP object.

Step 5: Replace placeholder service behavior

The scaffold gives you the feature shape, but not your actual business behavior.

For a first tutorial, an in-memory array is enough.

Update OrdersService.php so it stores orders in memory:

<?php

namespace Assegaiphp\KitchenOrdersApi\Orders;

use Assegaiphp\KitchenOrdersApi\Orders\DTOs\CreateOrderDTO;
use Assegaiphp\KitchenOrdersApi\Orders\DTOs\UpdateOrderDTO;

class OrdersService
{
  /**
   * @var array<int, array<string, mixed>>
   */
  private array $orders = [];

  public function findAll(): array
  {
    return array_values($this->orders);
  }

  public function create(CreateOrderDTO $dto): array
  {
    $id = count($this->orders) + 1;

    $order = [
      'id' => $id,
      'ticketNumber' => $dto->ticketNumber,
      'status' => $dto->status,
      'items' => $dto->items,
    ];

    $this->orders[$id] = $order;

    return $order;
  }

  public function update(int $id, UpdateOrderDTO $dto): ?array
  {
    if (!isset($this->orders[$id])) {
      return null;
    }

    $this->orders[$id]['status'] = $dto->status ?? $this->orders[$id]['status'];
    $this->orders[$id]['items'] = $dto->items ?? $this->orders[$id]['items'];

    return $this->orders[$id];
  }
}

This is not production persistence, and that is fine.

For the tutorial, the important thing is to feel the controller-service-DTO workflow clearly before the database enters the picture.

Step 6: Keep the controller thin

The generated controller should already be close to the right shape.

Keep the controller focused on HTTP concerns:

  • route
  • params
  • body binding
  • return value

That usually looks something like this:

#[Post]
public function create(#[Body] CreateOrderDTO $dto): array
{
  return $this->ordersService->create($dto);
}

That one method shows a lot of what Assegai is aiming for:

  • the route is close to the handler
  • the DTO is explicit
  • the controller does not contain the application logic

Step 7: Try the feature

Now test the flow.

Create an order:

POST /orders
Content-Type: application/json

{
  "ticketNumber": "KDS-1001",
  "status": "pending",
  "items": ["Burger", "Fries"]
}

Then list orders:

GET /orders

If you exported the OpenAPI document again, /docs should now reflect the DTO-backed request shape more clearly.

Step 8: What you just learned

Without adding a database yet, you have already used Assegai's main workflow:

  • scaffold with the CLI
  • let modules and generated structure define the feature boundary
  • let DTOs shape request input
  • keep controller methods thin
  • keep behavior in a provider
  • inspect the contract through generated docs

That is the core loop many Assegai apps repeat feature after feature.

When you switch this feature over to the ORM, keep three practical defaults in mind:

  • pass DTOs straight into create() and update() unless you genuinely need to reshape the data first
  • prefer save() as the default write path; insert() is still available, but save() is the smoother day-to-day choice for most feature code
  • prefer softRemove(...) for deletes, since entities already carry ChangeRecorderTrait

Step 9: Where to go next

Now you have two good next steps:

  1. add persistence with the ORM guides
  2. add authentication once the orders API should belong to real users

If you want persistence next, continue with Data and ORM.

If you want auth next, continue with Authentication.