0

RESTful vs REST — the difference between the standard and 'inspired by'

Intermediate5 min read·eng-13-005
interview

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/42 not /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
<?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