RESTful vs REST — the difference between the standard and 'inspired by'
Concept
RESTful vs REST — the difference between the theoretical standard and the practical "inspired by" implementation most APIs use.
REST: Roy Fielding's 6 architectural constraints (1.Stateless 2.Client-Server 3.Cacheable 4.Uniform Interface 5.Layered 6.Code on Demand). A truly REST-compliant API satisfies ALL 6, including HATEOAS.
RESTful: A colloquial term for "designed in the spirit of REST." Uses HTTP verbs (GET/POST/PUT/PATCH/DELETE), URLs as resource identifiers, and JSON. Does NOT necessarily implement HATEOAS or satisfy all 6 Fielding constraints.
Richardson Maturity Model — a practical spectrum of REST maturity:
- Level 0: HTTP as a tunnel (one endpoint, all requests are POSTs). XML-RPC, SOAP.
- Level 1: Resources. Multiple URLs, one per resource type. Still all POST.
- Level 2: HTTP Verbs. Correct verbs (GET, POST, PUT, DELETE) + HTTP status codes.
- Level 3: Hypermedia (HATEOAS). Responses include links to next possible actions.
Most production APIs are Level 2 — and call themselves "RESTful." Fielding himself has said Level 2 APIs are not REST. Industry has adopted "RESTful" to mean Level 2.
What makes an API "RESTful" in practice (Level 2):
- URLs are nouns (resources), not verbs.
/users/42not/getUser?id=42. - Correct HTTP methods. GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes.
- Proper status codes. 201 for creation, 404 for not found, 422 for validation errors.
- JSON request/response bodies with
Content-Type: application/json. - Stateless (JWT or API keys, not server sessions).
What separates good RESTful from bad:
- Consistent error format.
- Filtering, sorting, pagination via query parameters.
- Versioning strategy.
- Proper use of resource relationships (nested routes vs query params).
Code Example
<?php
// ❌ LEVEL 0 — HTTP as a tunnel (not RESTful at all)
Route::post('/api', function (Request $request) {
$action = $request->input('action');
if ($action === 'getUser') return User::find($request->input('id'));
if ($action === 'deleteUser') { User::find($request->input('id'))->delete(); return ['ok' => true]; }
if ($action === 'createUser') return User::create($request->input('data'));
// One endpoint, action in body — like SOAP/XML-RPC
});
// ❌ LEVEL 1 — Resources, but wrong verbs
Route::post('/users/get', fn($r) => User::find($r->id));
Route::post('/users/delete', fn($r) => User::find($r->id)->delete());
Route::post('/users/create', fn($r) => User::create($r->all()));
// Multiple URLs but action in the URL verb — RPC-style
// ✅ LEVEL 2 — RESTful (what everyone means by REST)
Route::apiResource('users', UserController::class);
// GET /users → index (list)
// POST /users → store (create)
// GET /users/{id} → show (read one)
// PUT /users/{id} → update (replace)
// PATCH /users/{id} → update (partial update)
// DELETE /users/{id} → destroy (delete)
class UserController extends Controller
{
public function index(Request $request)
{
return UserResource::collection(
User::filter($request->all())->paginate(15)
);
}
public function store(CreateUserRequest $request)
{
$user = User::create($request->validated());
return (new UserResource($user))
->response()
->setStatusCode(201) // 201 Created, not 200 OK
->header('Location', route('users.show', $user)); // Location header
}
public function destroy(User $user)
{
$user->delete();
return response()->noContent(); // 204 No Content — correct for delete
}
}
// ✅ LEVEL 3 — HATEOAS (true REST, rarely implemented)
// Response includes _links telling the client what it can do next
class OrderResource extends \Illuminate\Http\Resources\Json\JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'status' => $this->status,
'_links' => [
'self' => ['href' => route('orders.show', $this)],
'cancel' => $this->canBeCancelled()
? ['href' => route('orders.cancel', $this), 'method' => 'DELETE']
: null,
],
];
}
}
// With HATEOAS, a client needs NO pre-knowledge of the API — it follows links