De cero a API: Construyendo una API RESTFul con Laravel

Carlos Andrés Castañeda Osorio
11 min readMar 25, 2023

--

Por años el lenguaje de programación de PHP ha sido considerado una opción de desarrollo para la programación web, en el último tiempo se ha estigmatizado el uso de este lenguaje de programación dadás las malas prácticas implementadas por algunos desarrolladores que lo utilizan.

En este post vamos a ver cómo el uso del Framework de Laravel nos permite usar las mejores prácticas del mercado usando el lenguaje de programación PHP construyendo una API desde cero.

Todo el contenido de este post estará guiado por la documentación oficial de Laravel, a lo largo del post se usarán conceptos propios del framework para los cuales dejaré el enlace a su documentación oficial. La versión de Laravel usada para la construcción de la API es la 10.x.

Las API REST se basan en el protocolo de comunicación HTTP y hacen un uso correcto de los métodos que este trae en su definición (Get, Post, Put/Patch, Delete). Las ejecuciones de la API no deben considerar el estado del cliente, el estado de peticiones anteriores o algún indicador almacenado que haga variar su comportamiento. La comunicación debe ser sin estado (stateless). Para más información se recomienda leer este blog de API RESTFul.

Todo el código del presente post puede ser encontrado en mi repositorio de Github.

La colección de Postman también se encuentra dentro de los archivos del proyecto y tiene el nombre postman_collection.json.

Este post fue parte de los recursos utilizados en una conferencia de la comunidad Manizales Tech Talks en donde tuve la oportunidad de explicar y poner en práctica los conceptos aquí descritos, la grabación de la conferencia que tuvo una duración de una hora y media se puede encontrar en el canal oficial de youtube de Manizales Tech Talks.

Manos al código 👨🏽‍💻

Para iniciar es importante que la máquina que estamos usando tenga PHP instalado, este puede ser descargado de su documentación oficial. La versión utilizada en este post es PHP 8.2. Para validar la instalación y la versión que está ejecutando en nuestra m´aquina ejecutamos el siguiente comando:

php --version

También, es necesario contar con el gestor de paquetes composer, este puede ser descargado de su documentación oficial. Para comprobar que la instalación sea correcta, ejecutamos el siguiente comando:

composer --version

Con estos dos requerimientos cubiertos, es momento de crear nuestro proyecto de Laravel, para esto ejecutamos el siguiente comando:

composer create-project laravel/laravel events-api
cd events-api

Con el proyecto creado, es momento de crear nuestra base de datos y conectar nuestro proyecto, para este ejemplo crearemos una única tabla que contiene la siguiente estructura:

Lo primero que debemos hacer es modificar nuestro archivo de ambiente .env para colocar los datos de conexión a la base de datos, este archivo se encuentra en la carpeta raíz del proyecto que recién acabamos de crear. Las variables que deben ser modificadas en el archivo son:

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=events-api
DB_USERNAME=postgres
DB_PASSWORD=

Para rastrear los cambios de la base de datos, Laravel usa el concepto de migraciones, lo que haremos a continuación será crear un nuevo archivo de migración en donde escribiremos la estructura deseada de la tabla y Laravel, a través de su ORM de Eloquent, la creará en la base de datos. Para crear el archivo de migraciones ejecutamos el siguiente comando:

php artisan make:migration create_events_table

El nuevo archivo creado se puede encontrar en la carpeta database/migrations, este archivo contendrá toda la estructura de nuestra tabla, aquí se debe definir el tipo de dato de cada columna y las restricciones que pueda tener. Para conocer a detalle las opciones de tipos de columnas y restricciones disponibles se recomienda visitar la documentación oficial. El contenido del archivo se muestra a continuación:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('events', function (Blueprint $table) {
$table->id();
$table->string('event_name')->unique();
$table->timestamp('event_date');
$table->integer('event_max_capacity');
$table->string('event_speaker_name');
$table->string('event_location_name')->nullable();
$table->string('event_meetup_url')->nullable();
$table->boolean('event_is_virtual');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('events');
}
};

Con la estructura de nuestra tabla desarrollada, es momento que Laravel se encargue de crear la tabla en base de datos, para ello ejecutamos el siguiente comando en la consola:

php artisan migrate

Con este proceso finalizado, ahora en nuestra base de datos tenemos la tabla con la estructura definida en nuestro archivo, adicionalmente Laravel crea algunas tablas que trae por defecto para operaciones que tiene previamente establecidas.

A continuación, para que Laravel se conecte con la tabla requiere de un nuevo concepto llamado modelo, para más información visitar la documentación oficial. Los modelos de nuestro proyecto hacen referencia a las tablas de nuestra base de datos, por ende crearemos un nuevo modelo para nuestra tabla “events”. Ejecutamos el siguiente comando:

php artisan make:model Event

Nota: Por convención de Laravel, el nombre del modelo es el mismo de la tabla en singular, esto se puede cambiar pero se debe configurar un parámetro adicional.

Con esta acción realizada, se creó un nuevo archivo en la carpeta app/Models, a continuación añadiremos la propiedad fillable al modelo para poder realizar asignaciones masivas de datos.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Event extends Model
{
use HasFactory;

protected $fillable = [
"event_name",
"event_date",
"event_max_capacity",
"event_speaker_name",
"event_location_name",
"event_meetup_url",
"event_is_virtual"
];
}

Ahora es posible crear un nuevo registro en nuestra base de datos, para ello usamos el REPL de tinker. Para activar la herramienta en consola ejecutamos el siguiente comando:

php artisan tinker

A continuación en la consola interactiva que se abre escribimos el siguiente código:

$event = new \App\Models\Event();
$event->event_name = "De cero a API: Construyendo una API RESTFul con Laravel";
$event->event_date = "2023/03/25 10:00";
$event->event_max_capacity = 500;
$event->event_speaker_name = "Carlos Andres Castañeda Osorio";
$event->event_meetup_url = "https://www.meetup.com/es/manizalestechtalks/events/292151177";
$event->event_is_virtual = true;
$event->save();

Si todo hasta aquí se encuentra configurado de manera correcta, ahora en nuestra tabla debemos tener un nuevo registro almacenado.

Para realizar pruebas, es importante tener un buen número de registros almacenados en nuestra tabla, para introducir registros de prueba de manera masiva podemos usar los Factories de Laravel. Para iniciar ejecutamos el siguiente comando:

php artisan make:factory EventFactory

Con este comando, se creó un nuevo archivo en la carpeta database/factories, en este nuevo archivo definiremos la estructura de un registro usando datos falsos. El archivo queda como se muestra a continuación:

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/
class EventFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
"event_name" => fake()->unique()->words(5, true),
"event_date" => fake()->dateTime,
"event_max_capacity" => fake()->numberBetween(50, 1000),
"event_speaker_name" => fake()->name . fake()->lastName,
"event_location_name" => fake()->city,
"event_meetup_url" => fake()->url,
"event_is_virtual" =>fake()->boolean
];
}
}

Posterior a definir la estructura, es momento de crear los nuevos registros en la base de datos. Para ello, desde la consola interactiva de tinker, ejecutamos el siguiente comando:

\App\Models\Event::factory()->count(200)->create();

Luego de tener nuestra base de datos funcional, la tabla creada y registros almacenados, es momento de implementar la lógica de negocio. Para la construcción de nuestra API usaremos un controlador de tipo recurso, los métodos que trae por defecto, con su tipo de petición HTTP son:

  • index (GET): Método que retorna el listado completo del recurso solicitado, para nuestro ejemplo el listado de los eventos registrados en la base de datos. Para usar las mejores prácticas, en nuestro caso usaremos el concepto de paginación.
  • store (POST): Método que permite insertar un nuevo registro en la base de datos. Por parámetro recibe un objeto de tipo REQUEST que contiene el cuerpo de la petición y los datos recibidos.
  • update (PUT/PATCH): Método para actualizar un registro de la base de datos. La distinción original entre el tipo de petición PUT y PATCH es que mientras uno permite la actualización de algunos datos del registro, el otro solicita la actualización completa del mismo.
  • show (GET): Método para obtener los datos de un registro a partir de su ID único.
  • destroy (DELETE): Método para eliminar un registro de la base de datos a partir de su ID.

Con los métodos explicados y el objetivo claro, es momento de crear nuestro controlador, para ello ejecutamos el siguiente comando:

php artisan make:controller EventController --api --model=Event

Este controlador trae por defecto la firma de los métodos anteriormente mencionados, la implementación de los métodos se muestra a continuación:

<?php

namespace App\Http\Controllers;

use App\Models\Event;
use \Illuminate\Http\Response;
use Illuminate\Http\Request;

class EventController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
return Event::paginate();
}

/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$event = Event::create($request->all());
return response()->json(['event' => $event], Response::HTTP_CREATED);
}

/**
* Display the specified resource.
*/
public function show(Event $event)
{
return response()->json(['event' => $event], Response::HTTP_OK);
}

/**
* Update the specified resource in storage.
*/
public function update(Request $request, Event $event)
{
$event->update($request->all());
return response()->json(['event' => $event], Response::HTTP_OK);
}

/**
* Remove the specified resource from storage.
*/
public function destroy(Event $event)
{
$event->delete();
return response()->json(['event' => $event], Response::HTTP_ACCEPTED);
}
}

Nota: Es importante resaltar que se puede hacer uso de todos los parámetros que llegan en el request en el método store y update gracias a la asignación masiva.

Para que la API reciba las peticiones HTTP externas es necesario que tenga definidas las rutas y tipos de métodos HTTP soportados por cada una, para esto en nuestro archivo de routes/api.php escribimos el siguiente código:

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/


Route::apiResource('events', \App\Http\Controllers\EventController::class)
->middleware('auth:sanctum');

En este archivo se pueden escribir las rutas de manera separada, sin embargo usamos la utilidad apiResource que trae Laravel que crea una ruta por cada uno de los métodos creados en el controlador. Para ver las rutas y su respectivo método HTTP en la terminal escribimos el siguiente comando:

php artisan route:list

Luego de tener nuestra API funcionando con sus operaciones básicas, es momento de validar que los datos que estamos recibiendo se encuentran en el formato correcto, para esto Laravel ofrece la funcionalidad de los form request validation.

En nuestro caso crearemos una única clase de validación, sin embargo de verse necesario podría crearse una clase por cada método. Para crear nuestra clase Request de validación ejecutamos el siguiente comando:

php artisan make:request EventRequest

A continuación, en la clase creada en la carpeta app/Http/Request/EventRequest.php escribimos las siguientes reglas de validación:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class EventRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
"event_name" => ['required', 'unique:App\Models\Event,event_name', 'max:255'],
"event_date" => ['required', 'date'],
"event_max_capacity" => ['required', 'integer' ,'min:2', 'max:100'], // Multiple rules in Array format
"event_speaker_name" => ['required', 'max:255'],
"event_location_name" => 'nullable|max:255', //Multiple rules in String format
"event_meetup_url" => 'nullable|url',
"event_is_virtual" => ['required', 'boolean']
];
}
}

Con esto creado, es necesario que los métodos del controlador que van a utilizar el método sean modificados y que en lugar de recibir el par´ámetro $request de tipo Request, ahora reciba nuestra versión EventRequest que es una clase hija de Request, el cambio quedaría como se muestra a continuación:

public function store(EventRequest $request)
{
$event = Event::create($request->all());
return response()->json(['event' => $event], Response::HTTP_CREATED);
}

public function update(EventRequest $request, Event $event)
{
$event->update($request->all());
return response()->json(['event' => $event], Response::HTTP_OK);
}

Nota: Con esta modificación estamos aplicando uno de los conceptos de SOLID, el principio de sustitución de Liskov.

Con todo lo creado hasta el momento cumplimos con todos los requisitos para tener una API RestFul creada con el framework de Laravel, para el último paso de este post, agregaremos autenticación con tokens.

Autenticación con Laravel Sanctum 🔐

Para proteger nuestra API y no permitir que cualquiera con acceso a las rutas pueda realizar peticiones sin autorización usaremos el paquete de Laravel/sanctum , este paquete es un sistema de autenticación ligero avalado por Laravel para el uso de Token.

Antes que nada es importante aclarar que las API RESTFul son stateless, este concepto lo que define es que los usuarios no persisten una sesión dentro de la aplicación si no que para cada petición que realizan deben identificarse a través de un token que obtienen al iniciar sesión, este token debe estar contenido dentro de las cabeceras de cada petición http.

Para instalar la librería laravel/sanctum ejecutamos el siguiente comando:

composer require laravel/sanctum

A continuación debemos validar que nuestro modelo User (Que se encontraba creado desde el momento de iniciar nuestra aplicación) cuente con el Trait HasApiTokens y el mismo se encuentre en uso.

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;

/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];

/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];

/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}

Con esto realizado, es momento de crear un nuevo controlador para recibir la petición de inicio de sesión. Para ello ejecutamos el siguiente comando:

php artisan make:controller LoginController

Adicionalmente crearemos nuestro archivo Request para validar que los datos recibidos en el login sean los correctos, para ello ejecutamos el siguiente comando:

php artisan make:request LoginRequest

El archivo de validación de datos queda como se muestra a continuación:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class LoginRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'email'],
'password' => ['required'],
'name' => 'required'
];
}
}

Mientras que el controlador para inicio de sesión queda así:

<?php

namespace App\Http\Controllers;

use App\Http\Requests\LoginRequest;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
public function login(LoginRequest $request){
if (Auth::attempt($request->only(['email', 'password']))){
return response()->json([
'token' => $request->user()->createToken($request->name)->plainTextToken,
'message' => 'Success',
'status' => true
]);
}
return response()->json([
'message' => 'Unauthorized',
'status' => false
], Response::HTTP_UNAUTHORIZED);
}
}

Por último, para que nuestras rutas queden protegidas y nuestra ruta de login quede habilitada para ser usada, modificamos el archivo routes/api.php como se muestra a continuación:

<?php

use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/


Route::apiResource('events', \App\Http\Controllers\EventController::class)
->middleware('auth:sanctum');

Route::post('/login', [\App\Http\Controllers\LoginController::class, 'login']);

Es momento de probar que nuestra autenticación y protección de rutas se encuentra funcionado, para esto necesitamos tener usuarios registrados en nuestro sistema, para esto usaremos el Factory que trae por defecto nuestro proyecto y en la consola interactiva de tinker escribiremos el siguiente comando:

\App\Models\User::factory(10)->create();

Nota: Es importante revisar que en Factory, la contraseña por defecto de todos los usuarios es “password”.

Al hacer la petición http de tipo post de login del usuario, debemos obtener el valor del token que le corresponde al usuario, tomaremos este token y lo agregaremos como cabecera con la clave Authorization y en el valor de la cabecera colocaremos Bearer <token>. Con esta acción realizada en todas nuestras peticiones http recuperaremos el funcionamiento normal de nuestra API únicamente para aquellos usuarios que se encuentran autenticados.

Conclusiones

Luego de tener nuestra API construida en Laravel usando las mejores prácticas del mundo de la programación, algunas conclusiones que podemos tener de este trabajo son:

  • PHP no está muerto, es un lenguaje de programación que no deja de crecer y tener mayor rendimiento.
  • Los frameworks facilitan el uso de buenas prácticas del mundo de la programación, pero se debe complementar con una buena implementación por parte del equipo de desarrollo.
  • El uso de API se ha extendido en el mercado y el modelo RESTFul es ampliamente utilizado.
  • Laravel facilita y agiliza el desarrollo de software con sus múltiples herramientas y utilidades.

--

--

Carlos Andrés Castañeda Osorio

Apasionado por la tecnología 💻, el desarrollo de software 👨🏽‍💻 y la docencia 👨🏽‍🏫. Ing. en sistemas y computación y Mag. en ingeniería computacional 🎓.