Skip to main content

Basics of Laravel API Development — Controller, FormRequest, Eloquent, DI

About this chapter

This chapter is a supplementary chapter for those new to Laravel. If you already know Laravel, feel free to skip it. It covers only the Laravel knowledge you need for this book.

The relationship between an SPA and an API backend

This book assumes an architecture where the frontend (Vue/React) and the backend (Laravel) are separated.

  • Frontend: implementing UI/UX, state management, client-side routing, calling APIs
  • Backend: business logic, data persistence, authentication/authorization, validation

Example of calling the API from the frontend

// Calling the API from the frontend (Vue/React)
// This is the "callee" side that we will build in this book

// Example of calling the create-order API
const createOrder = async () => {
const response = await axios.post('/api/orders', {
shipping: {
prefecture: 'Tokyo',
city: 'Shibuya',
street: '1-1-1'
},
items: [
{ productId: 1, quantity: 2, unitPrice: 1000 }
]
});

// Example response: { orderId: 123, message: 'Order created' }
console.log(response.data.orderId);
};

REST API basics

HTTP methodPurposeExample
GETRetrieve dataGET /api/orders → list orders
POSTCreate dataPOST /api/orders → create an order
PUT/PATCHUpdate dataPUT /api/orders/1 → update an order
DELETEDelete dataDELETE /api/orders/1 → delete an order

The structure of a Laravel project

Let's look at the main directory layout of a Laravel project.

laravel-project/
├── app/ # Application code
│ ├── Http/
│ │ ├── Controllers/ # Classes that receive requests
│ │ ├── Requests/ # Validation rules
│ │ └── Resources/ # Response formatting
│ ├── Models/ # Eloquent models (DB operations)
│ └── Providers/ # Service providers (DI configuration)

├── config/ # Configuration files
├── database/
│ └── migrations/ # Table definitions
├── routes/
│ ├── api.php # API routing ← mainly used in this book
│ └── web.php # Web routing
└── .env # Environment variables (DB connection info, etc.)

API routing (routes/api.php)

Why learn routing?

Routing acts as the "entry point" that dispatches HTTP requests from the frontend to the appropriate code. By defining which URL runs which process, the API design becomes clear.

For details, see Laravel official documentation - Routing.

// routes/api.php
// The file that defines API endpoints (URLs)
// Routes defined here automatically get the /api prefix

use App\Http\Controllers\OrderController;

// Requests to /api/orders are handled by OrderController
Route::prefix('orders')->group(function () {
Route::get('/', [OrderController::class, 'index']); // GET /api/orders
Route::post('/', [OrderController::class, 'store']); // POST /api/orders
Route::get('/{id}', [OrderController::class, 'show']); // GET /api/orders/1
Route::put('/{id}', [OrderController::class, 'update']); // PUT /api/orders/1
Route::delete('/{id}', [OrderController::class, 'destroy']); // DELETE /api/orders/1
});

Creating an API endpoint

Basics of a controller

Why learn controllers?

A controller is the first place that receives a request from the frontend. It plays the role of "traffic control": validating the request, calling the necessary business logic, and returning an appropriate response.

For details, see Laravel official documentation - Controllers.

A controller is a class that receives an HTTP request and returns a response.

// app/Http/Controllers/OrderController.php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\JsonResponse;

// Controller class: processes HTTP requests
class OrderController extends Controller
{
/**
* Retrieve the list of orders
* GET /api/orders
*/
public function index(): JsonResponse
{
// Retrieve all records with Order::all() (Eloquent ORM, described later)
$orders = Order::all();

// Return a JSON response with response()->json()
return response()->json([
'data' => $orders
]);
}

/**
* Create a new order
* POST /api/orders
*/
public function store(CreateOrderRequest $request): JsonResponse
{
// Get validated data with $request->validated()
$order = Order::create($request->validated());

// Specify the HTTP status code as the second argument (201 = Created)
return response()->json([
'orderId' => $order->id,
'message' => 'Order created'
], 201);
}

/**
* Retrieve order details
* GET /api/orders/{id}
*/
public function show(int $id): JsonResponse
{
// findOrFail: automatically returns a 404 error if not found
$order = Order::findOrFail($id);

return response()->json([
'data' => $order
]);
}
}

Validation with FormRequest

Why learn validation?

Input from the frontend cannot always be trusted. Using FormRequest prevents invalid data from reaching your business logic and lets you manage error messages in one place.

For details, see Laravel official documentation - Validation.

FormRequest is a class responsible for validating input values.

// app/Http/Requests/CreateOrderRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

// FormRequest: a class that defines validation rules
// On a validation error, it automatically returns an error response with a 422 status
class CreateOrderRequest extends FormRequest
{
/**
* Define the validation rules
*/
public function rules(): array
{
return [
// 'field name' => ['rule1', 'rule2', ...]
'shipping.prefecture' => ['required', 'string', 'max:255'],
'shipping.city' => ['required', 'string', 'max:255'],
'shipping.street' => ['required', 'string', 'max:255'],

// Array validation
'items' => ['required', 'array', 'min:1'],

// items.* refers to each element of the array
'items.*.productId' => ['required', 'integer', 'min:1'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
'items.*.unitPrice' => ['required', 'integer', 'min:0'],
];
}

/**
* Custom messages for validation errors
*/
public function messages(): array
{
return [
'items.required' => 'Please select at least one product',
'items.min' => 'Please select at least one product',
'items.*.quantity.min' => 'Quantity must be 1 or greater',
];
}
}

Response formatting with API Resource

API Resource is a class that defines the JSON structure of the response.

// app/Http/Resources/OrderResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

// JsonResource: a class that defines the shape of the response
class OrderResource extends JsonResource
{
/**
* Define the structure of the response
* You can convert DB column names into API response names
*/
public function toArray($request): array
{
return [
'id' => $this->id,
'status' => $this->status,
// In the DB it is shipping_prefecture, but the API returns it as shippingAddress
'shippingAddress' => [
'prefecture' => $this->shipping_prefecture,
'city' => $this->shipping_city,
'street' => $this->shipping_street,
],
'totalAmount' => $this->total_amount,
'createdAt' => $this->created_at->format('Y-m-d H:i:s'),
];
}
}

// Example usage in a controller
public function show(int $id): OrderResource
{
$order = Order::findOrFail($id);
return new OrderResource($order);
}

HTTP status codes

CodeMeaningWhen to use
200OKSuccessful retrieval/update
201CreatedSuccessful creation
204No ContentSuccessful deletion (no response body)
400Bad RequestMalformed request
401UnauthorizedAuthentication error
403ForbiddenAuthorization error (no permission)
404Not FoundResource does not exist
422Unprocessable EntityValidation error
500Internal Server ErrorServer error

Introduction to Eloquent ORM

Why learn Eloquent ORM?

Instead of writing SQL directly, Eloquent ORM lets you operate on the database as PHP objects. This improves the readability of your code and makes working with relations more concise. That said, in DDD it is important not to bring Eloquent directly into your business logic.

For details, see Laravel official documentation - Eloquent ORM.

Eloquent ORM is a mechanism that lets you perform database operations with PHP classes.

Mapping between a model and a table

// app/Models/Order.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// Eloquent Model: a class that corresponds to a DB table
// Order class → orders table (plural, snake_case of the class name)
class Order extends Model
{
// To specify the table name explicitly
// protected $table = 'orders';

// Columns allowed for mass assignment (protection against mass assignment)
protected $fillable = [
'status',
'shipping_prefecture',
'shipping_city',
'shipping_street',
'total_amount',
];
}

Basic CRUD operations

// Create
$order = Order::create([
'status' => 'draft',
'shipping_prefecture' => 'Tokyo',
'shipping_city' => 'Shibuya',
'shipping_street' => '1-1-1',
]);

// Read
$order = Order::find(1); // Get one record by ID (null if not found)
$order = Order::findOrFail(1); // Get one record by ID (404 error if not found)
$orders = Order::all(); // Get all records
$orders = Order::where('status', 'draft')->get(); // Get multiple records by condition
$order = Order::where('status', 'draft')->first(); // Get one record by condition

// Update
$order = Order::find(1);
$order->status = 'confirmed';
$order->save();

// Or update in bulk
Order::where('id', 1)->update(['status' => 'confirmed']);

// Delete
$order = Order::find(1);
$order->delete();

// Or delete directly
Order::destroy(1);

Relations (associations)

// app/Models/Order.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Order extends Model
{
/**
* One-to-many relation: an order has multiple order lines
* orders table (1) → order_lines table (many)
*/
public function orderLines(): HasMany
{
// Associate via the order_id column of the OrderLine model
return $this->hasMany(OrderLine::class);
}
}
// app/Models/OrderLine.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class OrderLine extends Model
{
/**
* Many-to-one relation: an order line belongs to one order
*/
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
}
// Example usage

$order = Order::find(1);
$orderLines = $order->orderLines; // Get the related order lines

// Eager Loading (countermeasure for the N+1 problem)
$orders = Order::with('orderLines')->get();
What is the N+1 problem?

It is a problem where retrieving related data inside a loop issues one query per record, hurting performance. You can solve it with eager loading using with(). For details, see Chapter 13, "Repository pattern".

For details on migrations, see Laravel official documentation - Migrations.

The DI container and service providers

Why learn DI?

Dependency injection (DI) is a fundamental technique for lowering coupling between classes and writing testable code. In DDD and clean architecture, DI is essential for separating interfaces from implementations.

For details, see Laravel official documentation - Service Container.

What is dependency injection (DI)?

Dependency injection is a design pattern in which the objects a class needs are passed in from the outside.

// Bad: instantiating directly inside the class (tight coupling)
class OrderController
{
public function store(Request $request)
{
// Calling new OrderService directly
// Problems:
// - You cannot swap it out for a mock during tests
// - You are affected when the implementation of OrderService changes
$service = new OrderService();
$service->createOrder($request->all());
}
}

// Good: injecting through the constructor (loose coupling)
class OrderController
{
public function __construct(
// Laravel's DI container automatically injects the instance
private OrderService $service
) {}

public function store(Request $request)
{
// Use the injected instance
// You can swap it out for a mock during tests
$this->service->createOrder($request->all());
}
}

Interfaces and binding

In this book, we make use of interfaces in the repository pattern.

// Define an interface (a contract)
interface OrderRepositoryInterface
{
public function findById(int $id): ?Order;
public function save(Order $order): void;
}

// Implementation class
class EloquentOrderRepository implements OrderRepositoryInterface
{
public function findById(int $id): ?Order { /* ... */ }
public function save(Order $order): void { /* ... */ }
}

Binding in a service provider

A service provider is responsible for the configuration that ties an interface to its implementation class.

// app/Providers/RepositoryServiceProvider.php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
/**
* Binding of interfaces to implementation classes
* The setting "wherever OrderRepositoryInterface is needed,
* inject EloquentOrderRepository"
*/
public array $bindings = [
OrderRepositoryInterface::class => EloquentOrderRepository::class,
];
}

// Consumer side (UseCase, etc.)
class CreateOrderUseCase
{
public function __construct(
// Type-hint the interface
// → Laravel's DI container injects EloquentOrderRepository
private OrderRepositoryInterface $repository
) {}
}

The DI container automatically resolves the dependency and injects the instance.

The basics of API authentication

Why learn API authentication?

In an SPA (single-page application), you use token-based authentication rather than traditional session authentication. It is essential knowledge for controlling API access per user and building a secure application.

The concept of token-based authentication

The flow of token-based authentication

  1. On login, obtain a token and store it in localStorage or similar
  2. For APIs that require authentication, send the token in the Authorization header

Authentication with Laravel Sanctum

// routes/api.php

// Endpoints that do not require authentication
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);

// Endpoints that require authentication
// The auth:sanctum middleware requires token authentication
Route::middleware('auth:sanctum')->group(function () {
// APIs in this group return a 401 error without a token
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
Route::get('/user', [UserController::class, 'me']);
Route::post('/logout', [AuthController::class, 'logout']);
});
// app/Http/Controllers/AuthController.php

class AuthController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
$credentials = $request->validated();

// Authentication check
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'The email address or password is incorrect'
], 401);
}

$user = Auth::user();

// Issue an API token
$token = $user->createToken('api-token')->plainTextToken;

return response()->json([
'token' => $token,
'user' => $user,
]);
}

public function logout(Request $request): JsonResponse
{
// Delete the current token
$request->user()->currentAccessToken()->delete();

return response()->json([
'message' => 'Logged out'
]);
}
}

A review of PHP 8.x syntax

Why learn the new features of PHP 8?

Using the new features introduced in PHP 8.0 and later makes your code more concise and safer. In DDD especially, you make heavy use of immutable objects (value objects), so readonly and enum are extremely useful.

Let's review the PHP 8.x features used in this book.

Enable declare(strict_types=1); in real code

The samples in this book omit the <?php declaration for explanation, but in a real project we recommend enabling declare(strict_types=1); at the top of every PHP file. Because it forbids implicit type coercion, you can immediately catch issues such as passing a float to an argument that expects an int (a TypeError is raised), which prevents the rounding bugs that often occur in value-object calculations.

Constructor promotion (PHP 8.0+)

What becomes convenient: you can declare and initialize a property at once, which reduces verbose code. It is especially powerful for small classes such as value objects.

// Before: PHP 7 and earlier (verbose)
class Money
{
private int $amount;
private string $currency;

public function __construct(int $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}
}

// After: PHP 8.0+ (constructor promotion)
// Declare the property, the constructor argument, and the assignment at once
class Money
{
public function __construct(
private int $amount, // Argument declaration that also defines the property
private string $currency,
) {}
// The property declaration and the assignment in the constructor are no longer needed
// Assignments like $this->amount = $amount; are also done automatically
}

// Usage is the same
$money = new Money(1000, 'JPY');

Key points:

  • Adding an access modifier (private, protected, public) automatically defines it as a property
  • A property declaration goes from three lines to one, improving readability
  • It prevents typos (a mismatch between the property name and the argument name)

The readonly modifier (PHP 8.1+)

What becomes convenient: you can create a property that cannot be changed once set. You can safely handle data that must not be changed, such as DDD value objects and entity IDs.

class OrderId
{
public function __construct(
// readonly: cannot be changed once set (immutable)
private readonly int $value
) {}

public function value(): int
{
return $this->value;
}
}

$orderId = new OrderId(1);
echo $orderId->value(); // 1

// $orderId->value = 2; // Error: a readonly property cannot be changed
// Assignment outside the constructor causes a runtime error

Key points:

  • The value can only be set in the constructor; any change afterward is impossible
  • It prevents "unintended changes" that cause bugs
  • DDD has the principle that "value objects should be immutable," and readonly enforces it

A handy way to use a readonly class (PHP 8.2+):

// PHP 8.2 and later let you make an entire class readonly
readonly class OrderId
{
// All properties are automatically readonly
public function __construct(
private int $value
) {}
}

Enums (PHP 8.1+)

What becomes convenient: you can handle fixed values such as statuses in a type-safe way, rather than as string constants. It prevents assigning invalid values and enables IDE completion.

// Before: the traditional approach using string constants (not type-safe)
class Order
{
public const STATUS_DRAFT = 'draft';
public const STATUS_CONFIRMED = 'confirmed';

public string $status; // Any string can be assigned
}

$order->status = 'typo'; // A typo does not cause an error

// After: a Backed Enum (an enum that holds values)
enum OrderStatus: string
{
case DRAFT = 'draft'; // The value stored in the DB
case CONFIRMED = 'confirmed';
case SHIPPED = 'shipped';
case CANCELLED = 'cancelled';

// You can define methods on an enum too (collecting business logic)
public function label(): string
{
return match($this) {
self::DRAFT => 'Draft',
self::CONFIRMED => 'Confirmed',
self::SHIPPED => 'Shipped',
self::CANCELLED => 'Cancelled',
};
}

public function canBeCancelled(): bool
{
return $this === self::DRAFT || $this === self::CONFIRMED;
}

public function isCompleted(): bool
{
return $this === self::SHIPPED;
}
}

// Example usage
$status = OrderStatus::DRAFT;
echo $status->value; // 'draft' (the value stored in the DB)
echo $status->label(); // 'Draft' (the label for display)

if ($status->canBeCancelled()) {
// Processing for a cancellable state
}

// Convert from a string to an enum (convert a value retrieved from the DB into an enum)
$status = OrderStatus::from('confirmed'); // OrderStatus::CONFIRMED

// An invalid value causes an error
$status = OrderStatus::from('typo'); // A ValueError exception is thrown

// To avoid the error, use tryFrom()
$status = OrderStatus::tryFrom('typo'); // Returns null

Key points:

  • Values other than the defined cases cannot be assigned (type-safe)
  • IDE completion works, so you avoid typos
  • You can give the enum business logic (such as canBeCancelled())
  • You can separate the value stored in the DB (value) from the label for display (label())

Using enums in DDD:

Because an enum has properties close to a value object ("immutable and compared by value"), you can treat it with the same mindset as a value object (strictly speaking they are different concepts, but it is ideal for expressing a "set of fixed values" such as a status).

The null coalescing and nullsafe operators

What becomes convenient: you can write null checks concisely. The nullsafe operator in particular lets you safely access nested properties.

// Before: a traditional null check (verbose)
$name = isset($user->name) ? $user->name : 'Guest';

if ($user !== null && $user->address !== null) {
$city = $user->address->city;
} else {
$city = null;
}

// After: written concisely
// The null coalescing operator (??): returns the right side if the left side is null
$name = $user->name ?? 'Guest';

// The null coalescing assignment operator (??=): assigns if the variable is null
$name ??= 'Default Name'; // Assigns only when $name is null

// The nullsafe operator (?->): skips the rest of the chain if null (PHP 8.0+)
$city = $user?->address?->city; // Returns null if $user or $address is null

// The throw expression (PHP 8.0+): combine it with ?? to throw an exception concisely
$order = $this->repository->findById($id)
?? throw new OrderNotFoundException("Order not found");

Practical examples:

// A pattern often used in an API Resource
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name ?? 'No name',
'email' => $this->email,
// Safely access even if a relation is null
'companyName' => $this->user?->company?->name ?? 'No affiliation',
];
}

// Example usage in a repository
public function findById(int $id): Order
{
return $this->model->find($id)
?? throw new OrderNotFoundException("Order with ID {$id} was not found");
}

Arrow functions (PHP 7.4+)

What becomes convenient: you can write short anonymous functions concisely. They are used frequently in Laravel collection operations.

// Before: a traditional anonymous function (verbose)
$doubled = array_map(function ($n) {
return $n * 2;
}, [1, 2, 3]);

// After: an arrow function (a short anonymous function)
// fn($n) => $n * 2 automatically adds the return
$doubled = array_map(fn($n) => $n * 2, [1, 2, 3]);

// A pattern often used in Laravel
$orders = Order::all();
$ids = $orders->map(fn($order) => $order->id);

// Automatically captures outer variables (no use needed)
$threshold = 1000;

// Before: you must write use explicitly
$filtered = $orders->filter(function ($order) use ($threshold) {
return $order->total >= $threshold;
});

// After: an arrow function captures outer variables automatically
$filtered = $orders->filter(fn($order) => $order->total >= $threshold);

// Use a traditional anonymous function when you need multi-line processing
$results = $orders->map(function ($order) {
$total = $order->orderLines->sum('subtotal');
return [
'id' => $order->id,
'total' => $total,
];
});

Key points:

  • Write single-line processing concisely with an arrow function
  • Use a traditional anonymous function for multi-line processing
  • Because outer variables are captured automatically, use is not needed

Named arguments (PHP 8.0+)

What becomes convenient: the meaning of arguments becomes clear, improving readability. You can skip arguments that have default values in the middle.

// Before: traditional positional arguments
// It is hard to tell which value means what
$money = new Money(1000, 'JPY');

// After: named arguments (specifying the argument name)
// The meaning of each argument is clear at a glance
$money = new Money(
amount: 1000,
currency: 'JPY',
);

// You can also change the order
$money = new Money(
currency: 'JPY',
amount: 1000,
);

// You can skip arguments that have default values
function createOrder(
string $status = 'draft',
?string $note = null,
bool $priority = false,
) { /* ... */ }

// Before: you cannot skip an argument in the middle
createOrder('draft', null, true); // You must pass null to $note explicitly

// After: with named arguments you can specify only the arguments you need
createOrder(priority: true); // $status and $note use their default values

// Practical example: configuring validation rules
Route::post('/orders', [OrderController::class, 'store'])
->middleware('throttle:60,1'); // It is hard to tell what the numbers mean

Route::post('/orders', [OrderController::class, 'store'])
->middleware('throttle', maxAttempts: 60, decayMinutes: 1); // Clear

Key points:

  • Especially useful for functions with many arguments
  • The meaning of arguments is clear during code review
  • You can skip arguments that have default values in the middle

Summary

This chapter covered the basic Laravel knowledge you need to work through this book.

ConceptDescriptionHow it is used in this book
ControllerProcesses HTTP requestsPresentation layer
FormRequestInput validationPresentation layer
API ResourceResponse formattingPresentation layer
Eloquent ORMDatabase operationsInfrastructure layer (inside the repository)
DI containerDependency injectionLoose coupling of each layer
Service providerBinding configurationTying interfaces to implementations

From the next chapter on, we build on this knowledge to learn the concepts of DDD and clean architecture.

Preparing for the next chapter

The next chapter, "Why DDD is needed," looks at the problems that tend to arise in traditional Laravel development. The knowledge of "Controller" and "Eloquent Model" learned in this chapter is a prerequisite.