MVC tutorial · Chapter 04

Define and organize
routes.

Build a library's complete HTTP map and keep it readable, secure, and testable as it grows.

METHOD+PATH+POLICYACTION
Chapter project

Design every route for browsing, creating, updating, and deleting books, then verify their HTTP contracts.

Target map

Seven routes, one coherent resource.

GET/booksindex200
GET/books/createcreate200
POST/booksstore201 / 302
GET/books/{id}show200 / 404
GET/books/{id}/editedit200 / 404
PATCH/books/{id}update200 / 302
DELETE/books/{id}destroy204 / 302
04.1

A route is a contract

A route means more than an URL opening a page. It defines a client contract: method, path, parameters, protection, and action.

GET /books promises to read the collection. POST /books promises creation. They share a path but expose different intentions and responses.

The HTTP interface should be understandable without reading controller code. SQL, HTML, and long conditions do not belong in routes.

Principle to retain

Always say method and path: GET /books, not only /books.

phpaml — zsh
Route::get('/books', [BookController::class, 'index']);
04.2

Choose the HTTP method

The method announces the effect. GET reads, POST creates or triggers, PATCH partially changes, and DELETE removes.

GET must avoid intentional business side effects: refreshing must never create another book.

POST is not a stronger GET. PATCH /books/42 targets an existing resource; DELETE expresses removal.

Principle to retain

A normal link should generally be GET and never delete data.

04.3

Design URLs

An URL describes a resource, not PHP implementation. Prefer /books/42 over /showBook.php?id=42.

Use consistent plural collection names. An identifier selects one member; subpaths represent pages or relationships.

A good URL survives controller, database, and view replacements.

Principle to retain

Avoid technical verbs when the HTTP method already expresses the action.

04.4

Dynamic parameters and constraints

In /books/{id}, id comes from the path. You must still define acceptable values.

A numeric constraint keeps /books/hello away from an integer controller. It does not replace business validation: 42 may be valid but absent.

Distinguish no route, invalid parameter, and missing resource.

Principle to retain

Validate shape early, then existence and rules in the appropriate layer.

phpaml — zsh
Route::get('/books/{id}', [BookController::class, 'show'])
    ->whereNumber('id');
04.5

Named routes and URLs

A named route provides a stable identifier such as books.show so views do not copy /books/{id}.

When the path changes, name-based calls survive one declaration update.

Use a regular resource.action convention independent of visible labels.

Principle to retain

A name centralizes a contract otherwise copied into many views.

phpaml — zsh
$url = route('books.show', ['id' => $book->id]);
04.6

Groups, prefixes, and middleware

A group applies a common /admin prefix, auth middleware, or namespace.

Grouping avoids repeated protection and makes policy visible.

Middleware order matters. Security headers must also cover early 401, 403, and 429 responses.

Principle to retain

Declare shared protection at the nearest shared level without hiding it.

phpaml — zsh
Route::prefix('/admin')
    ->middleware(['auth', 'role:editor'])
    ->group(function (): void {
        Route::patch('/books/{id}', [BookController::class, 'update']);
        Route::delete('/books/{id}', [BookController::class, 'destroy']);
    });
04.7

Separate web and API

Web and API can share models while exposing different response contracts.

Web commonly uses sessions, CSRF, redirects, and HTML. APIs use JSON, tokens, explicit statuses, and /api/v1.

Share business rules through models or services while each surface adapts Request and Response.

Principle to retain

Shared business logic does not mean mixed HTTP contracts.

WEBSession · CSRF · HTML · Redirectroutes/webapp.php
APIToken · JSON · Status · /api/v1routes/api.php
04.8

Organize files

A small project fits routes/webapp.php. Hundreds of entries become an unreadable map.

Group by surface or feature: webapp.php, api.php, admin.php, then MovieRoute for route-per-controller APIs.

Keep loading discoverable and avoid unexplained filesystem magic.

Principle to retain

Add a file to clarify a real boundary, not to populate empty folders.

04.9

Distinguish 404 and 405

404 means no matching route or resource. 405 means the path exists but not for the received method.

GET /books/42 can be 404 when absent. POST /books/42 is 405 when only GET, PATCH, and DELETE exist.

A 405 may announce accepted methods with Allow.

Principle to retain

Diagnose method-path before resource existence.

404missing path or resource
405method not allowed
04.10

Test the map

A route is complete only when success, wrong methods, invalid parameters, missing resources, and middleware are verified.

Observe statuses, headers, redirects, and essential content without irrelevant visual coupling.

Verify sensitive routes remain inaccessible without authentication.

Principle to retain

Every important contract branch deserves a reproducible test.

Assembly

The complete CRUD map.

Read it as an HTTP table of contents. Every declaration should lead to a short, focused controller method.

phpaml — zsh
Route::get('/books', [BookController::class, 'index'])->name('books.index');
Route::get('/books/create', [BookController::class, 'create'])->name('books.create');
Route::post('/books', [BookController::class, 'store'])->name('books.store');
Route::get('/books/{id}', [BookController::class, 'show'])->name('books.show');
Route::get('/books/{id}/edit', [BookController::class, 'edit'])->name('books.edit');
Route::patch('/books/{id}', [BookController::class, 'update'])->name('books.update');
Route::delete('/books/{id}', [BookController::class, 'destroy'])->name('books.destroy');

Final project

Build and test BookRoute.

  1. Write seven CRUD routes without business logic.
  2. Name them using books.action.
  3. Constrain id to a positive integer.
  4. Protect creation, update, and deletion.
  5. Add the /api/v1/books JSON version.
  6. Test success, 404, 405, validation, and denied access.

Reasoned solution

Verify contracts, not only syntax.

Quality depends on boundaries: no database query in routes, no secrets, coherent methods, stable names, explicit constraints, and visible protection.

Why /books/create when POST /books creates?

GET /books/create displays the form without changing data. POST /books then processes its submission.

Should there be one route class per controller?

For a medium API, MovieRoute with MovieController is clear. For a small site, webapp.php is enough. Optimize discovery without empty folders.

Chapter 03Chapter 05 · Coming soon 🔒