Introduction — what we build, and what we leave out
Being able to write one endpoint in Laravel and being able to operate that API in production are two different skills.
Code written straight from a tutorial works correctly as long as you are the one calling it. It breaks when several requests arrive at once, when an external service slows down, and when someone adds one column to a table. This series is written to close that gap.
The gap between code that works and code you can operate
Look at the order API below. It is the shape you see in introductory articles.
public function store(Request $request)
{
$validated = $request->validate([
'product_id' => 'required|integer',
'quantity' => 'required|integer|min:1',
'customer_email' => 'required|email',
]);
$stock = DB::table('stocks')->where('product_id', $validated['product_id'])->first();
if ($stock->quantity < $validated['quantity']) {
return response()->json(['message' => 'Not enough stock'], 400);
}
$order = Order::create($validated);
DB::table('stocks')
->where('product_id', $validated['product_id'])
->decrement('quantity', $validated['quantity']);
Mail::raw('Thank you for your order', function ($message) use ($validated) {
$message->to($validated['customer_email'])->subject('Your order has been received');
});
return response()->json($order, 201);
}
Read it and nothing about the flow looks off. It validates, checks stock, creates the order, decrements stock, and sends a confirmation email. As long as requests arrive one at a time, the results are exactly what you expect.
It does not match the table structure we build in chapter 3. Read it not as something to run as is, but as something to look at and ask "where does this break?"
This code has four problems that are guaranteed to surface in production.
Stock gets double-allocated
Say there is one unit left in stock and two people order at the same time. Right after the first person's stock check, and before stock is decremented, the second person's check runs. From the second person's point of view there is still one unit, so the check passes. Two orders get created and stock ends up at -1.
This is not bad luck; it is guaranteed to happen as traffic grows. The cause is the structure itself: another request can slip in between the check and the update. Chapter 14 closes it with transactions, row locks, and conditional updates.
The email send time becomes the response time
Mail::raw() does not move to the next line until the send to the mail server finishes. If sending takes 3 seconds, the order API takes 3 seconds to respond. If the mail server is down, the order itself fails.
Recording an order and sending a confirmation email have different conditions for success. Chapters 15 and 16 move the latter outside the request.
Returning the model as is leaks information later
response()->json($order, 201) turns every column of the Order model into JSON. It may be fine now, but the moment the operations team adds a column called internal_memo six months from now, its contents appear in the API response. The person who added the column had no idea it would be exposed publicly.
What goes into a response has to be decided separately from what suits the model. Chapter 7 covers this.
Anyone can call it
This endpoint has no authentication. customer_email comes in with the request, so writing someone else's email address lets you create an order in their name. Chapter 11 adds authentication and chapter 12 adds authorization.
This series moves forward by picking up these problems one at a time. Every chapter has a section called "What matters in production," describing what breaks when you use the feature for real.
The subject: an e-commerce order system
Across all 22 chapters we grow a single e-commerce order system. It covers products, stock, orders, and order lines, plus registered users who arrive partway through.
This subject was chosen because the features the series wants to cover show up naturally. A product listing becomes a caching target, and an order confirmation email becomes a motive for moving work onto a queue. Auto-cancelling unpaid orders calls for the task scheduler, and product images call for file storage. Rather than "invent a subject to explain this feature," the order is: grow the subject, and the features become necessary.
Table design is settled in chapter 3. For now it is enough to grasp the shape of the progression: each chapter adds one more thing.
The API this series builds
In the end you have the following endpoints.
| Method | Path | What it does | Auth |
|---|---|---|---|
| GET | /api/v1/products | Product listing (paginated, filterable) | Not required |
| GET | /api/v1/products/{product} | Product detail | Not required |
| POST | /api/v1/auth/register | Sign-up | Not required |
| POST | /api/v1/auth/login | Log in (issue a token) | Not required |
| POST | /api/v1/auth/logout | Log out (revoke the token) | Required |
| POST | /api/v1/orders | Create an order | Required |
| GET | /api/v1/orders | Your own order list | Required |
| GET | /api/v1/orders/{order} | Order detail | Required |
| GET | /api/v1/orders/{order}/receipt | Download the receipt PDF | Required |
| POST | /api/v1/products/{product}/image | Upload a product image | Required (a dedicated token) |
| POST | /api/v1/webhooks/payment | Receive a notification from the payment service | Signature verification |
Behind the endpoints, the following also run.
- An async job that fires when an order is placed (sending the confirmation email)
- Async jobs that fire when payment is confirmed (the shipping instruction and generating the receipt PDF)
- A cache of the product listing, invalidated when a product is updated
- A daily batch that auto-cancels unpaid orders
- Rate limits on login attempts and on public endpoints
The environment this guide assumes
| Item | Value |
|---|---|
| Laravel | 13 (released March 2026) |
| PHP | 8.3 or later (Laravel 13 requires 8.3 through 8.5) |
| Database | SQLite (enough to run everything locally) |
| PHP extension | GD (needed for the image upload tests in chapter 19) |
| Other | Composer, and basic terminal use |
Laravel bumps its major version once a year. Bug fixes for 13 run to Q3 2027, and security fixes to March 17, 20281. Choosing a version is part of your operations plan, not a matter of implementation convenience.
Be careful reading older articles. A little while back several configuration files were reorganized and the way you write things changed. Chapter 2 covers what changed and how, in one place. Articles that assume the pre-reorganization layout no longer match the current file structure at all.
What this series does not cover
Frontend features such as Blade, Livewire, and Inertia are not covered at all. Every chapter is about an API that returns JSON. Read it assuming the screens are built separately.
Broadcasting (Reverb / Echo) is out of scope too. Configuring only the server side leaves you unable to confirm the behavior without writing a receiving client in JavaScript. That does not fit the policy of leaving out the frontend, so the series does not touch it.
Design theory is also out of scope. Tactical patterns from domain-driven design and how to separate layers in Clean Architecture are not covered. What this series shows is how far you can get with Laravel's standard directory structure as it is.
How this fits with the existing Laravel guides
This site has two other guides that cover Laravel. Their roles differ, so reading them in order builds up your understanding.
| Guide | What it covers | How it relates to this series |
|---|---|---|
| PHP Class Design Guide | Principles such as encapsulation, single responsibility, and SOLID | It is the foundation for the criteria used in chapter 6 when extracting business logic. Go there when you want the principles themselves |
| A Practical Guide to Laravel × DDD × Clean Architecture | Value objects, aggregates, repositories, and separating layers | The guide to read after this one. Move on when Laravel's standard structure starts feeling cramped |
| Database Design Guide | Normalization, E-R diagrams, and index design | The theoretical backing for building the tables in chapter 3 |
Where several guides cover the same feature, this series takes how you write it in the standard structure and the DDD guide takes which layer it belongs in. For transactions, for example, this series covers how to write DB::transaction() and row locks, while the DDD guide covers where to draw the boundary.
Summary
- An API written straight from a tutorial breaks under concurrent access, external service latency, and schema changes
- This series grows an e-commerce order system over 22 chapters, closing each of those failure modes one at a time
- It targets Laravel 13 and PHP 8.3. Frontend features and design theory are out of scope
- Every chapter has a "What matters in production" section, and that is the heart of the series
What to read next
The next chapter, The request lifecycle and the skeleton of Laravel 13, follows how Laravel processes a single request. Middleware, the exception handler, and service providers all plug in somewhere along that path. Having the map first means that when a new concept turns up in a later chapter, you know where it goes.
Exercises
If you were writing the order API from the top of this chapter today, where would you start fixing it?
Sample answer
There is no single correct answer, but you can give a reason for the order you fix things in.
The first thing to close is the double-allocated stock. Once data is corrupt, recovering it later takes manual work. A slow response inconveniences users, but the data stays correct. An information leak is serious, but there is nothing to leak yet (internal_memo does not exist).
The axis, in other words, is "can this be undone once it happens?" Close the damage you cannot undo first.
Because of how the chapters are ordered, this series does not cover transactions until chapter 14. Until then we deliberately carry the broken code around. Each chapter opens by explaining why the order is what it is.