Here's where we dive into the actual structure of your API. Not the abstract philosophy we discussed before, but the concrete, practical patterns you'll use on every single endpoint. When I say "architecture," I don't mean over-engineered diagrams or complex abstractions that sound smart in meetings. I mean the specific way you organize your code, where validation lives, how responses get formatted, what controllers actually do, so that tomorrow you can add a new endpoint as easily as today, without rethinking anything.
Think about the last time you inherited some messy code. Maybe it was someone else's API. Maybe it was your own API from three years ago. Either way, you probably remember the feeling: every endpoint was different. Validation happened three different ways. Responses had inconsistent structures. Controllers were 200 lines long mixing HTTP concerns with business logic. You had to read each endpoint like an archaeologist uncovering ancient ruins.
You don't want to be the person who writes code like that. And you definitely don't want to be the person who inherits it.
The good news: you can avoid this entirely with the right patterns. This chapter covers the four foundational patterns that will appear in every single endpoint you build: Controllers, FormRequests, Response Wrappers, DTOs, Resources, Mappers and Facades.
These aren't new concepts you need to learn. They're not complex abstractions. They're Laravel's built-in concepts, applied consistently. You'll recognize all of them. The power comes from using them the same way, everywhere, so consistency becomes automatic.
By the end of this chapter, you'll understand the blueprint that your entire API will follow. Every license endpoint, every user endpoint, every domain endpoint, they'll all use the same patterns. That consistency is what lets you ship endpoint 50 as fast as endpoint 1.
Controllers: Keep Them Simple
Here's a controversial opinion: most controllers are too fat. They do too much. Validation, business logic, transformation, response formatting, all in one method.
Controllers should be obvious and boring.
Fat controllers become impossible to test and harder to read and change. The next developer sees that validation happens inline in the controller, and they copy it. They see business logic mixed in, and they add more. By endpoint number ten, you have ten different patterns. By endpoint one hundred, you're maintaining a codebase where every endpoint does things differently.
// Bad: Fat controller doing everything
public function create(Request $request): Response
{
if (!$request->has('name') || !$request->has('domain')) {
return response()->json(['error' => 'Name and domain required'], 422);
}
$client = new GuzzleHttp\Client();
$response = $client->post('https://api.statamic.com/licenses', [
'json' => [
'name' => $request->input('name'),
'domain' => $request->input('domain'),
],
'headers' => [
'Authorization' => 'Bearer ' . config('services.statamic.key')
],
]);
$data = json_decode($response->getBody(), true);
$formatted = [
'key' => $data['key'],
'name' => $data['name'],
'domain' => $data['domain'],
'status' => $data['status'],
];
return response()->json([
'data' => $formatted
], 201);
}
This is the enemy. Not complexity, inconsistency. Inconsistency means every developer has to understand the specific pattern of every endpoint they touch. It means code reviews take hours because you're not just checking logic, you're parsing a different structure on every endpoint. It means new developers spend weeks learning the unwritten rules of your API instead of shipping features.
The rule is simple: Controllers receive requests and return responses. Everything else is someone else's job.
// Good: Thin controller delegating work
public function create(CreateLicenseRequest $request): Responsable
{
return new ModelResponse(
data: new LicenseResource(
resource: LicenseDTOMapper::toDTO(
LicenseFacade::addLicense(
name: $request->validated('name'),
domain: $request->validated('domain'),
)
),
),
status: Response::HTTP_CREATED, //201
);
}
Each step is clear:
FormRequesthandles validationResponsablehandles the HTTP (status code, headers, structure)Resourcehandles JSON formatting (DTO to API response)Mapper::toDTO()handles transformation (raw data to typed object)Facadehandles the business logic
You read it once and understand what it does. No surprises. No buried logic. When something needs to change, a new field, different validation, a different provider, you know exactly where to look. When you need to add logging, it goes in the facade. When you need to add a permission check, it goes in the FormRequest. Each concern lives in one place.
Every controller looks thin. New developers join and immediately understand every endpoint because they've seen this pattern once. Code reviews are fast because the structure is predictable. Testing is simple because each component is isolated. Your controller's job is to receive the HTTP request, delegate to services or facades for business logic, map DTOs if needed, format the response, and return it. That's it. Everything else belongs somewhere else.