Content Index
- What is a REST API?
- Server and Client
- What a REST API Can Do
- REST and its Rules
- HTTP and Methods
- APIs in General
- Status Codes (HTTP Status Codes)
- What exactly is JSON?
- API Installation
- Creating the API Controller
- Controllers and routes
- Explanation of the above code
- Handle exceptions
- How does this flow work?
- Implement custom methods
- Get All
- Consume by slug
REST APIs provide a flexible and lightweight way to integrate applications; that is, in which we can communicate two or more applications.
Let's look at the key concepts:
An API is a set of rules that define how applications or devices can connect and communicate with each other.
A REST API is nothing more than an API that conforms to REST design principles and uses HTTP requests (GET, POST, PUT, PATCH, DELETE) to consume and manage this data.
The REST architecture is nothing more than a set of restrictions or limitations between the main ones, we have:
- Separation between client and server; that is, two separate applications.
- Stateless, that is, for best practices, we should not use sessions or similar mechanisms.
- Cacheable, to make it more efficient, we can cache the response to the same resource.
- A uniform interface both for its consumption in which each resource must have an established URI and responses returned in JSON or XML.
In practice, a Rest Api is nothing more than an application or module of the same, which has a set of implemented functions that can be consumed through a URL and they can perform CRUD operations to manage the data. The Rest Api is consumed through HTTP requests and they always return the same data type; JSON mainly.
What is a REST API?
I’m going to tell you a little “grandpa story.”
Let's assume we have our super application in Laravel. We already have our entities, we can create records, edit them, delete them.
Now, imagine we want to consume that information from another application, for example, an application made in Vue, React, Angular, Astro, or any of the 20 existing JavaScript frameworks.
But not only that. It could also be a mobile application:
- Android (Android Studio)
- React Native
- Flutter (my favorite)
- iOS with Swift or SwiftUI
Think, for example, of Gmail: you can view your emails from the browser, but also from your mobile. What is happening there is that the mobile application (the client) connects to a server, and that server exposes an API.
That is basically a REST API: A mechanism that allows different applications to communicate with each other.
Server and Client
In most cases, we have:
- A server, which in our case will be Django.
- A client, which can be a web application or a mobile app.
Although we usually connect server with client, you could also consume a REST API from another application in Django, or from Laravel, Flask, etc. In short: a REST API allows us to interconnect applications, regardless of the technology they use.
What a REST API Can Do
A REST API is not just for creating, reading, updating, or deleting data. It can also:
- Send emails
- Execute processes
- Automate tasks
- Expose services to third parties
Everything is programmable.
The key idea I want you to keep is this:
A REST API is a mechanism to connect different applications, usually a server with one or more clients.
REST and its Rules
A REST API is not just about “returning data.” It has rules.
For example:
- GET → query data
- POST → create records
- PUT / PATCH → update (fully or partially)
- DELETE → delete
Although you could technically use GET to create data or POST to query, it is not recommended, for security reasons and best practices.
REST is, basically, a set of rules that tell us how we should perform that communication.
HTTP and Methods
Here is something important:
HTML only understands GET and POST, but the HTTP protocol (the one we use when browsing the internet, with HTTP or HTTPS) supports many more methods:
- GET
- POST
- PUT
- PATCH
- DELETE
And precisely REST APIs are based on HTTP, not on HTML.
APIs in General
An API is an application programming interface. There are many types of APIs:
- SOAP
- GraphQL
- REST
They all serve to communicate applications, but each has its own rules, advantages, and disadvantages, just like when you compare Django with Laravel.
A Rest API is nothing more than an interface between systems that uses HTTP to get and send data or perform operations on that data in many formats such as XML and JSON.
To create a Rest Api, we can use exactly the same logic that we have used so far; the only difference is where our routes will be registered, which would no longer be in the web.php file but in the api.php file.
For this chapter, we are going to create a new project in Laravel although, you can use the same project that we have used until now, if you decide to create a new project, you must copy the migrations, request and Post and Category models.
Status Codes (HTTP Status Codes)
In an API, we no longer return a visual error page, but rather a numeric code that the client application (Vue, Flutter, etc.) must interpret:
- 200 OK: Everything went well.
- 201 Created: The record was created successfully.
- 400 / 422: Client error (incorrect data sent or validation failed).
- 401 Unauthorized: The user did not send a valid token.
- 404 Not Found: The resource does not exist.
- 500 Internal Server Error: Our Laravel server crashed due to a logic error.
What exactly is JSON?
If you've never looked at it in detail, it's simply a text format based on key-value pairs.
- If it's a single object, we use curly braces {}.
- If it's a list of data (like the index), we use square brackets [] to represent an array.
Example of what your new route returns:
[
{
"id": 1,
"title": "Laravel 11",
"slug": "laravel-11"
},
{
"id": 2,
"title": "Vue.js",
"slug": "vue-js"
}
]API Installation
As of Laravel 11, the api.php file is not published, to publish it, we must execute the artisan commands:
$ php artisan install:apiThe api.php file contains the routes for creating a Rest Api; these routes are designed to be stateless, so requests entering the application through these routes must be authenticated using tokens and will not have access to the session state.
This will publish the api.php file:
routes\api.php
And Sanctum will be installed in the process which is a package to enable authentication which we will discuss later:
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
- Downloading laravel/sanctum (vX.X)
- Installing laravel/sanctum (vX.X): Extracting archiveWith this, to access the routes, we must enter the domain URL followed by the api prefix:
<DOMAIN>/api/<RESOURCE>For example:
http://larafirststeps.test/api/category
If you want to customize the routes to indicate a prefix other than the API:
use Illuminate\Support\Facades\Route;
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware('api')
->prefix('webhooks')
->name('webhooks.')
->group(base_path('routes/webhooks.php'));
},
)Remember to run the migrations if they exist:
$ php artisan migrateMore information in:
https://laravel.com/docs/master/routing#routing-customization
Creating the API Controller
In a REST API, we stop returning HTML (Blade views) and instead return JSON, which is the standard, lightweight, and machine-readable format. To keep things organized, we'll store these controllers in a specific folder:
app/Http/Controllers/Api
Controllers and routes
We created the controllers for the APIs:
$ php artisan make:controller Api/PostController -m PostAnd
$ php artisan make:controller Api/CategoryController -m CategoryWe created the routes in:
routes/api.php:
Route::resource('category', App\Http\Controllers\Api\CategoryController::class)->except(["create", "edit"]);
Route::resource('post', App\Http\Controllers\Api\PostController::class)->except(["create", "edit"]);To avoid conflicts between the route names in the dashboard and the API route names, we will prefix the API route names:
routes/api.php:
Route::group(['as' => 'api.'], function () {
Route::resource('category', CategoryController::class)->only(['index']);
Route::resource('post', PostController::class)->only(['index']);
});The controllers look like:
Api/CategoryController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Category\PutRequest;
use App\Http\Requests\Category\StoreRequest;
use App\Models\Category;
use Illuminate\Http\JsonResponse;
class CategoryController extends Controller
{
public function index(): JsonResponse
{
return response()->json(Category::paginate(10));
}
public function store(StoreRequest $request): JsonResponse
{
return response()->json(Category::create($request->validated()));
}
public function update(PutRequest $request, Category $category): JsonResponse
{
$category->update($request->validated());
return response()->json($category);
}
public function destroy(Category $category): JsonResponse
{
$category->delete();
return response()->json(['message' => 'Deleted'], 204);
}
}And for the post:
Api/PostController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Post\PutRequest;
use App\Http\Requests\Post\StoreRequest;
use App\Models\Post;
use Illuminate\Http\JsonResponse;
class PostController extends Controller
{
public function index(): JsonResponse
{
return response()->json(Post::paginate(10));
}
public function store(StoreRequest $request): JsonResponse
{
return response()->json(Post::create($request->validated()));
}
public function show(Post $post): JsonResponse
{
return response()->json($post);
}
public function update(PutRequest $request, Post $post): JsonResponse
{
$post->update($request->validated());
return response()->json($post);
}
public function destroy(Post $post): JsonResponse
{
$post->delete();
return response()->json(['message' => 'Deleted'], 204);
}
}Explanation of the above code
You can see that we left out some features like the edit and create forms; since, in a Rest Api, these intermediate views are not necessary to create the resources, let us remember that these are used to paint the form and nothing else, and in a Rest Api, this would not be necessary.
Finally, we always return a response in JSON format with: response()->json().
Which receives two parameters:
- The data.
- The status code.
To display the category with all the information and not just the identifier:
{
"id": 1,
"title": "Post 1",
"slug": "post-1",
"description": "test",
"content": "test",
"image": "test",
"posted": "yes",
"category_id": 1,
"created_at": null,
"updated_at": null,
"category": {
"id": 1,
"title": "cate 1 new",
"slug": "cate-1"
}
}We can indicate that it brings the relationship when pagination:
Api/PostController.php
public function index()
{
return response()->json(Post::with('category')->paginate(10));
}Although response()->json() returns a 200 (OK) status by default, best practices suggest being more specific:
- 201 (Created): Ideal for the store method, indicating that the resource was created successfully.
- 204 (No Content): This is the standard for the destroy method. It means the operation was successful but there is no content to display (because the record no longer exists).
Te recomiendo familiarizarte con los códigos de estado HTTP.
Handle exceptions
To handle exceptions, specifically those that occur when the records do not exist at the time of the search, for example:
"message": "No query results for model [App\\Models\\Category] cate-1asas",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",If you change the APP_DEBUG variable to false in your .env file, Laravel will stop displaying those technical details and will show a generic error page. However, for an API, we want finer control.
Starting with Laravel 11, we have the management of global configurations in a single file:
bootstrap\app.php
So, from the method:
withExceptionsWe handle exceptions, from the aforementioned method we can capture the exceptions that we want to customize:
bootstrap\app.php
return Application::configure(basePath: dirname(__DIR__))
***
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (NotFoundHttpException $e, $request) {
if($request->expectsJson()){ // or $request->wantsJson()
return response()->json('Not found',404);
}
});
})->create();We specify specific exception handling for the exception that is occurring which is:
Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpExceptionAnd if you expect to receive a JSON response ($this->expectsJson()) which is the format that we are going to use from the Rest Api; and in this case, we generate a custom exception like the one we implemented above. From the file above, you can customize the behavior of any other exceptions you consider.
How does this flow work?
- Capture: The render method specifically detects the exception you define (you can Control-click on the class to see all the exceptions Laravel offers in the Vendor folder).
- Discrimination: We use $request->expectsJson(). This is vital because if the user is browsing the Dashboard (web) and something is missing, we want them to see the Blade 404 page, not JSON code.
- Response: If it's an API request (thanks to the Accept: application/json header we configured in Postman), we return our custom response.
Implement custom methods
In this section, we are going to create some specific methods for the consumption of posts or categories.
Get All
Now, let’s create a couple of methods to get all the records without pagination:
app\Http\Controllers\Api\PostController.php
public function all(): JsonResponse
{
return response()->json(Post::get());
}And
app\Http\Controllers\Api\CategoryController.php
public function all(): JsonResponse
{
return response()->json(Category::get());
}The routes:
routes\api.php
Route::get('post/all', [PostController::class, 'all']);
Route::get('category/all', [CategoryController::class, 'all']);Consume by slug
To consume by slug, we can directly use the show command, but, varying the parameter in the URL, so that it is NOT the ID which is the default but the slug field:
And for URLs, something like the following:
routes\api.php
Route::get('post/slug/{post:slug}', [App\Http\Controllers\Api\PostController::class, 'show']);
Route::get('category/slug/{category:slug}', [App\Http\Controllers\Api\CategoryController::class, 'show']);You can change the URL, but it is important that it does not conflict with an existing one, for example, the show route.
If we consume the above method, we will have something like the following:
// http://larafirststeps.test/api/post/slug/xgyxsfyabgyefiaubhog
{
"id": 1,
"title": "xGYxsFYABgyEFiAuBhOg",
"slug": "xgyxsfyabgyefiaubhog",
"description": "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Vitae ",
"content": "<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Vitae aperiam culpa veritatis quasi laudantium mollitia quidem est blanditiis ullam illum cupiditate suscipit, quia, itaque quaerat? Iure debitis laudantium aliquam maxime!</p>",
"image": null,
"posted": "yes",
"created_at": "2026-03-14T18:20:14.000000Z",
"updated_at": "2026-03-14T18:20:14.000000Z",
"category_id": 11,
"category": {
"id": 11,
"title": "Categoria 10",
"slug": "categoria-10",
"created_at": "2026-03-14T18:20:14.000000Z",
"updated_at": "2026-03-14T18:20:14.000000Z"
}
}If you want it to bring the associated category, you can use the scheme of:
$post = Post::with("category")->where("slug", $slug)->firstOrFail();Or
$post = Post::where("slug", $slug)->firstOrFail();
$post->category;It is important to note the second case, Laravel works with a lazy loading scheme, which means that it will not bring the relationship data unless you request it; in the second case, we are consuming the category of the selected post and therefore, it queries the database and is registered in the post object.
The firstOrFail() method fetches a single record based on the condition (just like the first() method), if it doesn’t find it then it gives a 404 error.
Another variation for the previous case is to define the method as follows:
public function slug(Post $post): JsonResponse // $slug
{
//$post = Post::with("category")->where("slug", $slug)->firstOrFail();
$post->category;
return response()->json($post);
}It is important to note that we now have the message injected into the method (that is, as a parameter, this is known as dependency injection) therefore, to tell Laravel that what it is going to receive is the slug and to do the mapping to post; we indicate this by the routes:
Route::get('post/slug/{post:slug}', [PostController::class, 'slug']);For the categories, we are going to carry out the same procedure:
public function slug(Category $category): JsonResponse
{
return response()->json($category);
}And the route:
Route::get('category/slug/{category:slug}', [CategoryController::class, 'slug']);Section source code:
https://github.com/libredesarrollo/book-course-laravel-base-api-11/releases/tag/v0.1