Índice de contenido
- ¿Qué es una API REST?
- Servidor y cliente
- Qué puede hacer una API REST
- REST y sus reglas
- HTTP y los métodos
- APIs en general
- Códigos de Estado (HTTP Status Codes)
- ¿Qué es un JSON exactamente?
- Instalación de la API
- Creando el API Controller
- Controladores y rutas
- Explicación del código anterior
- Manejar excepciones
- ¿Cómo funciona este flujo?
- Implementar métodos personalizados
- Obtenerlas todas
- Consumir por el slug
Una Rest Api no es más que una interfaz entre sistemas que usa HTTP para obtener y enviar datos o generar operaciones sobre esos datos en varios formatos como XML y JSON.
Para crear una Rest Api, podemos emplear exactamente la misma lógica que manejamos hasta ahora; la única diferencia es donde van a estar registradas nuestras rutas, que ya no estarían en el archivo de web.php si no en el archivo de api.php.
Para este capítulo, vamos a crear un nuevo proyecto en Laravel aunque, puedes emplear el mismo proyecto que hemos empleado hasta ahora, si decides crear un nuevo proyecto, debes de copiar las migraciones, request y modelos de Post y Categoría.
Las API RESTs brindan una forma flexible y liviana de integrar aplicaciones; es decir, en la cual podemos comunicar dos o más aplicaciones.
Veamos los conceptos claves:
Una API es un conjunto de reglas que definen cómo las aplicaciones o dispositivos pueden conectarse y comunicarse entre sí.
Una API REST no es más que una API que se ajusta a los principios de diseño de REST y utiliza las peticiones de HTTP (GET, POST, PUT, PATCH, DELETE) para realizar el consumo y gestión de estos datos.
La arquitectura REST no es más que un conjunto de restricciones o limitaciones entre las principales, tenemos:
- Separación entre el cliente y el servidor; es decir, dos aplicaciones aparte.
- Sin estado, es decir, por buenas prácticas, no debemos usar sesiones o mecanismos similares.
- Cacheable, para hacer más eficiente, podemos guardar en caché la respuesta a un mismo recurso.
- Una interfaz uniforme tanto para el consumo de la misma en la cual cada recurso debe de tener una URI establecida y respuestas devueltas en JSON o XML.
En la práctica, una Rest Api no es más que una aplicación o módulo de la misma, la cual cuenta con un conjunto de funciones implementadas que pueden ser consumidas mediante una URL y las mismas pueden hacer operaciones CRUD para administrar los datos. La Rest Api es consumida mediante peticiones HTTP y siempre devuelven un mismo tipo de dato; JSON principalmente.
¿Qué es una API REST?
Te voy a contar una pequeña “historia de abuelo”.
Supongamos que tenemos nuestra súper aplicación en Laravel. Ya tenemos nuestras entidades, podemos crear registros, editarlos, eliminarlos.
Ahora bien, imagina que queremos consumir esa información desde otra aplicación, por ejemplo, una aplicación hecha en Vue, React, Angular, Astro, o cualquiera de los 20 frameworks de JavaScript que existen.
Pero no solo eso. También podría ser una aplicación móvil:
- Android (Android Studio)
- React Native
- Flutter (mi favorita)
- iOS con Swift o SwiftUI
Piensa, por ejemplo, en Gmail: tú puedes ver tus correos desde el navegador, pero también desde el móvil. Lo que está pasando ahí es que la aplicación móvil (el cliente) se conecta a un servidor, y ese servidor expone una API.
Eso es básicamente una API REST: Un mecanismo que permite que distintas aplicaciones se comuniquen entre sí.
Servidor y cliente
En la mayoría de los casos, tenemos:
- Un servidor, que en nuestro caso será Django.
- Un cliente, que puede ser una aplicación web o una app móvil.
Aunque normalmente conectamos servidor con cliente, también podrías consumir una API REST desde otra aplicación en Django, o desde Laravel, Flask, etc. En resumen: una API REST nos permite interconectar aplicaciones, sin importar la tecnología que usen.
Qué puede hacer una API REST
Una API REST no sirve solo para crear, leer, actualizar o eliminar datos. También puede:
- Enviar correos
- Ejecutar procesos
- Automatizar tareas
- Exponer servicios a terceros
Todo es programable.
La idea clave que quiero que te quedes es esta:
Una API REST es un mecanismo para conectar aplicaciones distintas, normalmente un servidor con uno o varios clientes.
REST y sus reglas
Una API REST no es solo “devolver datos”. Tiene reglas.
Por ejemplo:
- GET → consultar datos
- POST → crear registros
- PUT / PATCH → actualizar (total o parcialmente)
- DELETE → eliminar
Aunque técnicamente podrías usar GET para crear datos o POST para consultar, no es lo recomendado, por temas de seguridad y buenas prácticas.
REST es, básicamente, un conjunto de normas que nos dicen cómo debemos hacer esa comunicación.
HTTP y los métodos
Aquí hay algo importante:
HTML solo entiende GET y POST, pero el protocolo HTTP (el que usamos cuando navegamos por internet, con HTTP o HTTPS) soporta muchos más métodos:
- GET
- POST
- PUT
- PATCH
- DELETE
Y precisamente las APIs REST se basan en HTTP, no en HTML.
APIs en general
Una API es un interfaz de programación de aplicaciones. Existen muchos tipos de APIs:
- SOAP
- GraphQL
- REST
Todas sirven para comunicar aplicaciones, pero cada una tiene sus propias reglas, ventajas y desventajas, igual que pasa cuando comparas Django con Laravel.
Códigos de Estado (HTTP Status Codes)
En una API, ya no devolvemos una página de error visual, sino un código numérico que la aplicación cliente (Vue, Flutter, etc.) debe interpretar:
- 200 OK: Todo salió bien.
- 201 Created: El registro se creó con éxito.
- 400 / 422: Error del cliente (datos mal enviados o validación fallida).
- 401 Unauthorized: El usuario no ha enviado un token válido.
- 404 Not Found: El recurso no existe.
- 500 Internal Server Error: Nuestro servidor Laravel explotó por un error de lógica.
¿Qué es un JSON exactamente?
Si nunca has visto uno a fondo, es simplemente un formato de texto basado en pares clave-valor.
- Si es un solo objeto, usamos llaves {}.
- Si es una lista de datos (como el index), usamos corchetes [] para representar un array.
Ejemplo de lo que devuelve tu nueva ruta:
[
{
"id": 1,
"title": "Laravel 11",
"slug": "laravel-11"
},
{
"id": 2,
"title": "Vue.js",
"slug": "vue-js"
}
]Instalación de la API
A partir de Laravel 11, el archivo de api.php no se encuentra publicado, para publicarlo, debemos de ejecutar los comandos de artisan:
$ php artisan install:apiEl archivo api.php contiene las rutas para la creación de una Api Rest; estas rutas están diseñadas para no tener estado, por lo que las solicitudes que ingresan a la aplicación a través de estas rutas deben autenticarse mediante tokens y no tendrán acceso al estado de la sesión.
Con esto, se publicará el archivo de api.php:
routes\api.php
Y se instalará Sanctum en el proceso que es un paquete para habilitar la autenticación que trataremos más adelante:
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 archiveYa con esto, para acceder a las rutas, debemos de colocar la URL del dominio seguido del prefijo de api:
<DOMAIN>/api/<RESOURCE>Por ejemplo:
http://larafirststeps.test/api/category
Si quieres personalizar las rutas para indicar otro prefijo que no sea el de 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'));
},
)Más información en:
https://laravel.com/docs/master/routing#routing-customization
Recuerda ejecutar las migraciones en caso de que existan:
$ php artisan migrateCreando el API Controller
En una REST API dejamos de devolver HTML (vistas Blade) para devolver JSON, que es el formato estándar, ligero y fácil de leer para las máquinas. Para mantener el orden, guardaremos estos controladores en una carpeta específica:
app/Http/Controllers/Api.
Controladores y rutas
Creamos los controladores para las APIs:
$ php artisan make:controller Api/PostController -m PostY
$ php artisan make:controller Api/CategoryController -m CategoryCreamos las rutas en:
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"]);Para evitar conflictos entre los nombre de las rutas en el dashboard y las de API, vamos a colocarle un prefijo a los nombre de las rutas de la API:
routes/api.php:
Route::group(['as' => 'api.'], function () {
Route::resource('category', CategoryController::class)->only(['index']);
Route::resource('post', PostController::class)->only(['index']);
});Los controladores lucen como:
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);
}
}Y para el de 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);
}
}Explicación del código anterior
Puedes ver que prescindimos de algunos métodos como el de los formularios de edit y create; ya que, en una Api Rest, no es necesario estas vistas intermedias para crear los recursos, recordemos que, estas son empleadas para pintar el formulario y nada más, y en una Rest Api, esto no sería necesario, solamente mantener los procesos de crear y editar.
Finalmente, siempre devolvemos una respuesta en formato JSON con: response()->json().
La cual recibe dos parámetros:
- Los datos.
- El código de estado.
Para exponer la categoría con toda la información y no solo el identificador:
{
"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"
}
}Podemos indicar que traiga la relación al momento de hacer la paginación:
Api/PostController.php
public function index()
{
return response()->json(Post::with('category')->paginate(10));
}Aunque response()->json() devuelve por defecto un estado 200 (OK), las buenas prácticas sugieren ser más específicos:
- 201 (Created): Ideal para el método store, indicando que el recurso se creó con éxito.
- 204 (No Content): Es el estándar para el método destroy. Significa que la operación fue exitosa pero no hay contenido que mostrar (porque el registro ya no existe).
Te recomiendo familiarizarte con los códigos de estado HTTP.
Manejar excepciones
Para manejar las excepciones, específicamente aquellas que ocurren cuando no existen los registros al momento de la búsqueda, por ejemplo:
"message": "No query results for model [App\\Models\\Category] cate-1asas",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",Si cambias la variable APP_DEBUG=false en tu archivo .env, Laravel dejará de mostrar esos detalles técnicos y mostrará una página de error genérica. Sin embargo, para una API, queremos un control más fino.
A partir de Laravel 11, tenemos el manejo de las configuraciones globales en un solo archivo:
bootstrap\app.php
Así que, desde el método de:
withExceptionsManejamos las excepciones, desde el mencionado método podemos capturar las excepciones que queremos personalizar:
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();Especificamos un manejo de excepción específico para la excepción que está ocurriendo que es la de:
Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpExceptionY si se espera recibir una respuesta JSON ($this->expectsJson()) el cual es el formato que vamos a emplear desde la Api Rest; y en este caso, generamos una excepción personalizada como la que implementamos anteriormente. Desde el archivo anterior, puedes personalizar el comportamiento de cualquier otra excepción que consideres.
¿Cómo funciona este flujo?
- Captura: El método render detecta específicamente la excepción que definas (puedes hacer Control+Click en la clase para ver todas las que Laravel ofrece en la carpeta Vendor).
- Discriminación: Usamos $request->expectsJson(). Esto es vital porque si el usuario está navegando en el Dashboard (web) y algo no se encuentra, queremos que vea la página 404 de Blade, no un código JSON.
- Respuesta: Si es una petición de API (gracias al header Accept: application/json que configuramos en Postman), devolvemos nuestra respuesta personalizada.
Implementar métodos personalizados
En este apartado, vamos a crear algunos métodos específicos para el consumo de los posts o categorías.
Obtenerlas todas
Ahora, vamos a crear un par de métodos para obtener todos los registros sin paginación:
app\Http\Controllers\Api\PostController.php
public function all(): JsonResponse
{
return response()->json(Post::get());
}Y
app\Http\Controllers\Api\CategoryController.php
public function all(): JsonResponse
{
return response()->json(Category::get());
}Las rutas:
routes\api.php
Route::get('post/all', [PostController::class, 'all']);
Route::get('category/all', [CategoryController::class, 'all']);Consumir por el slug
Para consumir por el slug, podemos usar directamente el de show, pero, variando el parámetro en la URL, que NO sea el ID que es el por defecto si no el campo de slug:
Y para las URL, algo como las siguientes:
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']);Puedes variar la URL, pero es importante que no cause conflicto con otra ya existente, por ejemplo, la de show.
Si consumimos el método anterior, tendremos algo como lo siguiente:
// 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"
}
}Si quieres que traiga la categoría asociada, puedes usar el esquema de:
$post = Post::with("category")->where("slug", $slug)->firstOrFail();O
$post = Post::where("slug", $slug)->firstOrFail();
$post->category;Importante notar el segundo caso, Laravel trabaja con un esquema lazy loading, lo que significa, es que, no va a traer los datos de relaciones al menos que los solicites; en el segundo caso, estamos consumiendo la categoría del post seleccionado y por ende, realiza la consulta a la base de datos y queda registrado en el objeto de post.
El método firstOrFail() trae un único registro según la condición (al igual que el método de firts()), si no lo encuentra, entonces da un error 404.
Otra variación para el caso anterior, es definir el método de la siguiente manera:
public function slug(Post $post): JsonResponse // $slug
{
//$post = Post::with("category")->where("slug", $slug)->firstOrFail();
$post->category;
return response()->json($post);
}Importante notar que, ahora tenemos el post inyectado en la método (es decir, como parámetro, a esto se le conoce como inyección de dependencia) por lo tanto, para indicar a Laravel que lo que va a recibir es el slug y que haga el mapeo al post; esto, lo indicamos por las rutas:
Route::get('post/slug/{post:slug}', [PostController::class, 'slug']);Para las categorías, vamos a realizar el mismo procedimiento:
public function slug(Category $category): JsonResponse
{
return response()->json($category);
}Y la ruta:
Route::get('category/slug/{category:slug}', [CategoryController::class, 'slug']);Código fuente:
https://github.com/libredesarrollo/book-course-laravel-base-api-11/releases/tag/v0.1