Principios SOLID aplicado a una API REST en Laravel

Principios SOLID aplicados a un APIRest

En el mundo del desarrollo moderno, las APIs REST son el corazón que impulsa la comunicación entre diferentes sistemas. Ya sea que estés construyendo una aplicación móvil o una web compleja, una API bien diseñada es esencial para asegurar la interoperabilidad y escalabilidad de tus aplicaciones.

Pero ¿cómo asegurarnos de que nuestra API REST sea robusta, mantenible y fácil de escalar? Aquí es donde entran en juego los principios SOLID, una guía indispensable para cualquier desarrollador que quiera mantener su código limpio y bien estructurado.

En este artículo, te mostraré cómo combinar estos principios con las mejores prácticas RESTful para crear una API REST en Laravel que gestione libros de manera eficiente y profesional. Y para que no te pierdas nada, te iré dejando enlaces a los recursos adicionales y al código en GitHub. ¡Vamos allá!

1. Entendiendo la Arquitectura RESTful

Antes de lanzarnos a aplicar SOLID, es crucial entender bien los fundamentos de REST:

  • Sistema en Capas: REST permite diseñar aplicaciones en capas, lo que mejora la escalabilidad y seguridad. Laravel ya incorpora esta idea en su arquitectura MVC (Modelo, Vista, Controlador), asegurando que cada componente tenga una responsabilidad clara.
  • Separación Cliente-Servidor: REST promueve la independencia entre cliente y servidor. En Laravel, esto se refleja en la separación clara entre rutas, controladores y vistas.
  • Métodos HTTP: Los métodos GET, POST, PUT y DELETE son la base para interactuar con recursos. En Laravel, estas operaciones se manejan fácilmente a través de controladores bien definidos.
  • Apatridia: En REST, cada solicitud es independiente, lo que significa que debe contener toda la información necesaria para procesarse. Esto se implementa en Laravel mediante tokens de autenticación como JWT.
  • Interfaz Uniforme: Las URLs deben ser claras y seguir convenciones estándar. Por ejemplo, /api/books para todos los libros y /api/books/{id} para un libro específico.
  • Caché y Código Bajo Demanda: Finalmente, REST permite el uso de caché para mejorar la eficiencia. En Laravel, se pueden configurar respuestas cacheables utilizando middleware, lo que reduce la carga en el servidor y mejora la velocidad de respuesta.

1.2 Conectividad en REST

Además de los principios básicos, REST también enfatiza la conectividad entre recursos y representaciones.

  • Mensajes Autodescriptivos: Es importante que cada respuesta de la API sea autodescriptiva, proporcionando toda la información necesaria para que el cliente pueda interpretarla correctamente. En Laravel, esto se logra estructurando las respuestas de manera clara y utilizando JSON como formato estándar.
  • Interacciones Basadas en Recursos: También es fundamental que cada recurso en la API se represente claramente, siguiendo buenas prácticas en el manejo de URLs y rutas. Por ejemplo, en nuestra API de libros, cada libro se trata como un recurso individual accesible a través de rutas específicas.
  • Manipulación a Través de Representaciones: Además, en REST, la manipulación de recursos se realiza a través de representaciones, es decir, las interacciones con el recurso (como obtener, crear, actualizar o eliminar) se logran utilizando los métodos HTTP adecuados. En Laravel, esto se implementa de manera directa a través de los controladores.
  • HATEOAS (Hipermedia como Motor del Estado de la Aplicación): Finalmente, el concepto de HATEOAS permite que los clientes interactúen con la API de manera más dinámica y autoexplorable. En Laravel, se puede implementar HATEOAS proporcionando enlaces en las respuestas de la API que permitan a los clientes descubrir nuevas acciones posibles.
Principios SOLID
Principios SOLID

2. Características Adicionales de una API RESTful

Además de los métodos básicos, una API RESTful también debe implementar otras características importantes.

2.1 Paginación, Filtrado y Ordenación

Para manejar grandes volúmenes de datos, es fundamental implementar paginación, filtrado y ordenación en las respuestas de la API.

  • Paginación: Laravel facilita la paginación con métodos como paginate(), que dividen los resultados en páginas manejables.
public function index()
{
    return Book::paginate(10); // Devuelve 10 libros por página
}

  • Filtrado y Ordenación: Implementa filtrado y ordenación utilizando parámetros de consulta en la URL.
public function index(Request $request)
{
    $query = Book::query();

    if ($request->has('author')) {
        $query->where('author', $request->input('author'));
    }

    if ($request->has('title')) {
        $query->where('title', 'like', '%' . $request->input('title') . '%');
    }

    return $query->orderBy('title', 'asc')->paginate(10);
}

2.2 Control de Versiones

Para mantener la compatibilidad hacia atrás, es crucial implementar versiones en la API. Esto se puede hacer utilizando prefijos en las rutas, como /api/v1/books.

Route::middleware('auth:sanctum')->group(function (){
    // Auth
        Route::post('logout', [LoginController::class, 'logout'])->name('logout');
    // Books
        Route::apiResource('v1/books', BookController::class);
    // Categories
        Route::apiResource('v1/categories', CategoryController::class)->only(['index', 'show']);
    // Tags
        Route::apiResource('v1/tags', TagController::class)->only(['index', 'show']);
});

2.3 Seguridad

La seguridad es una preocupación central en cualquier API. Es importante implementar CORS, autenticación y limitación de velocidad para proteger la API.

  • CORS: Laravel proporciona soporte nativo para CORS, permitiendo configurar políticas de acceso en cors.php.
  • Autenticación: Utiliza Passport o JWT para asegurar que solo usuarios autenticados puedan acceder a ciertos endpoints.
  • Limitación de Velocidad: Laravel también permite configurar limitación de solicitudes usando middleware como throttle.

3. Mejores Prácticas en una API RESTful

A continuación, abordaremos algunas de las mejores prácticas para mantener una API RESTful segura y eficiente.

3.1 Monitoreo y Registro

Es esencial monitorear el uso de la API y registrar las solicitudes y respuestas para detectar problemas y garantizar la seguridad.

public function show($id)
{
    Log::info('Se ha consultado el libro con ID: ' . $id);
    return Book::findOrFail($id);
}

3.3 Interacción Segura

Para asegurar que solo los usuarios autorizados puedan interactuar con la API, implementa mecanismos de autenticación y autorización. Además de Policy y Gates, también se puede controlar el acceso a recursos a través del método authorize de los Form Request Validation. Aprende más sobre el método authorize en este artículo.

// Middleware para asegurar que solo usuarios autenticados puedan eliminar un libro
public function destroy($id)
{
    $this->authorize('delete', Book::class);
    Book::destroy($id);
    return response(null, 204);
}

4. Implementación Inicial: Una API REST Básical

Aplicando los Métodos HTTP en una API RESTful

Comencemos con una implementación sencilla para gestionar libros en nuestra API. Para garantizar que la API siga los principios RESTful, es esencial aplicar correctamente los métodos HTTP.

  • GET: Este método se utiliza para recuperar recursos. En nuestra API de libros, una solicitud GET a /api/books devolverá la lista de todos los libros, mientras que un GET a /api/books/{id} devolverá un libro específico.

   public function show(Book $book): JsonResponse
   {
        $this->authorize('show', $book);

        $book =$book->load('category', 'tags', 'user');
        return response()->json(new BookResource($book));
   }

  • POST: Usado para crear un nuevo recurso. En Laravel, esto se implementa en el controlador con una solicitud POST a /api/books, que creará un nuevo libro en la base de datos.
  • En esta implementación podemos ver el uso de funcionalidades como los Form Request Validation y lo Resources de Laravel. Te dejo el enlace a los artículos para que puedas saber más en profundidad para qué se usan y como. Pero resumidamente, el primero permite crear request adaptadas con sus propias validaciones y autorizaciones por cada caso de uso y los Resources, permiten estandarizar y estructurar las respuestas. Te invito a echar un vistazo a cada uno de los artículos para profundizar un poco más.
 public function store(CreateBookRequest $request): JsonResponse
 {
    $book = $request->user()->books()->create($request->all());

    if ($tags = json_decode($request->tags)) {
        $book->tags()->attach($tags);
     }

    $book->image_thumbnail = $request->file('image')->store('books', 'public');
    $book->save();

    return response()->json(new BookResource($book), Response::HTTP_CREATED);
 }

  • PUT: Este método se utiliza para actualizar un recurso existente. En nuestra API, se implementa permitiendo que una solicitud PUT a /api/books/{id} actualice la información de un libro específico.

Aquí podemos encontrar otra funcionalidad de seguridad que son las Policy de Laravel. Aquí te dejo un artículo que explica más en detalle en que consisten. Pero en definitiva, válida si el usuario tiene permisos para realizar esa acción.

    public function update(Book $book, UpdateBookRequest $request): JsonResponse
    {
        $this->authorize('update', $book);

        $book->update($request->all());

        if ($tags = json_decode($request->tags)) {
            $book->tags()->sync($tags);
        }

        if ($request->file('image')) {
            $book->image_thumbnail = $request->file('image')->store('books', 'public');
            $book->save();
        }

        return response()->json(new BookResource($book), Response::HTTP_OK);
    }

  • DELETE: Finalmente, este método se utiliza para eliminar un recurso. En nuestra API, una solicitud DELETE a /api/books/{id} eliminará un libro de la base de datos.
    public function destroy(Book $book): JsonResponse
    {
        $this->authorize('delete', $book);

        $book->delete();

        return response()->json(null, Response::HTTP_NO_CONTENT);
    }

Este controlador maneja las operaciones CRUD básicas para libros. Sin embargo, si lo miramos de cerca, notaremos que estamos mezclando responsabilidades y que hay margen de mejora. ¡Es hora de aplicar los principios SOLID!

5. Refactorizando con SOLID: Paso a Paso

En este punto, tenemos un controlador muy chulo que nos permite crear, editar, borrar o mostrar uno o varios libros. Pero, ¿Tu crees que estamos cumpliendo con los principios SOLID en nuestro controlador? Vamos a descubrirlo!!

1. Single Responsibility Principle (SRP)

El principio de responsabilidad única establece que una clase debe tener una única razón para cambiar. En este controlador, las responsabilidades están mezcladas: el controlador se encarga de la lógica de negocio, la validación de los datos y la gestión de archivos.

Paso 1: Mover la lógica de negocio a un servicio

Crearé un BookService que maneje la creación, actualización y eliminación de libros. Esto separará la lógica de negocio del controlador.

class BookService {
    public function createBook(array $data, User $user): Book {
        $book = $user->books()->create($data);
        if (isset($data['tags'])) {
            $book->tags()->attach($data['tags']);
        }
        return $book;
    }

    public function updateBook(Book $book, array $data): Book {
        $book->update($data);
        if (isset($data['tags'])) {
            $book->tags()->sync($data['tags']);
        }
        return $book;
    }

    public function storeBookImage(Book $book, mixed $image): Book {
        if ($image !== null) {
            $book->image_thumbnail = $image->store('books', 'public');
            $book->save();
        }

        return $book;
    }
}

Paso 2: Llamar a nuestro servicio en el controlador

El controlador ahora puede delegar la lógica al servicio y ahora quedaría así:

    public function store(CreateBookRequest $request): JsonResponse
    {
        $book = $this->bookService->createBook($request->all(), $request->user());
        if ($request->file('image')) {
            $book = $this->bookService->storeBookImage($book, $request->file('image'));
        }
        return response()->json(new BookResource($book), Response::HTTP_CREATED);
    }

2. Open/Closed Principle (OCP)

El principio de abierto/cerrado sugiere que las clases deben estar abiertas para su extensión, pero cerradas para su modificación. Esto no está directamente violado aquí, pero podemos mejorar la extensibilidad mediante el uso de interfaces.

interface BookServiceInterface {
    public function createBook(array $data, $user): Book;
    public function updateBook(Book $book, array $data): Book;
    public function storeBookImage(Book $book, $image): Book;
}

Implementa la interfaz en el servicio:


class BookService implements BookServiceInterface 
{
          // Implementación de los métodos...
}

3. Liskov Substitution Principle (LSP)

El LSP está más relacionado con la herencia, donde las clases derivadas deben ser sustituibles por sus clases base. En este caso, no estamos utilizando herencia directamente, pero al implementar interfaces, aseguramos que cualquier clase que implemente BookServiceInterface pueda sustituir a BookService sin romper la funcionalidad.

4. Interface Segregation Principle (ISP)

El ISP sugiere que las clases no deben depender de métodos que no usan. Aunque nuestra BookServiceInterface tiene todos los métodos que nuestro controlador necesita, es un recordatorio de que, en implementaciones más complejas, debemos dividir interfaces grandes en más pequeñas y específicas.

5. Dependency Inversion Principle (DIP)

El principio de inversión de dependencias establece que las clases deben depender de abstracciones y no de implementaciones concretas. Por ese motivo vamos a dar una vuelta más a nuestra refactorización. Aprovechando que hemos creado una interfaz BookServiceInterface para que nuestro servicio BookService la implementara. Será el momento de inyectarla en nuestro controlador.

Un apunte, para poder realizar la inyección de dependencias en Laravel, tendrás que realizar una pequeña configuración. En Laravel, la mejor práctica es registrar la asociación de la interfaz con la implementación en un Service Provider. Puedes usar el AppServiceProvider o crear un proveedor específico para los servicios.

// app/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\BookServiceInterface;
use App\Services\BookService;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(BookServiceInterface::class, BookService::class);
    }

    public function boot()
    {
        //
    }
}

Inyectamos BookServiceInterface en nuestro controlador:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Requests\Book\CreateBookRequest;
use App\Http\Requests\Book\UpdateBookRequest;
use App\Http\Resources\Book\BookResource;
use App\Models\Book;
use App\Src\Repository\BookServiceInterface;
use App\Src\Services\BookService;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class BookController extends Controller
{
    public function __construct(private BookServiceInterface $bookService)
    {

    }
    public function index(): JsonResponse
    {
        $books = Auth::user()->books()->with('category', 'tags', 'user')->get();

        return response()->json(['data' => BookResource::collection($books)]);
    }

    public function store(CreateBookRequest $request): JsonResponse
    {
        $book = $this->bookService->createBook($request->all(), $request->user());
        if ($request->file('image')) {
            $book = $this->bookService->storeBookImage($book, $request->file('image'));
        }
        return response()->json(new BookResource($book), Response::HTTP_CREATED);
    }

    public function show(Book $book): JsonResponse
    {
        $this->authorize('show', $book);

        $book =$book->load('category', 'tags', 'user');
        return response()->json(new BookResource($book));
    }

    public function update(Book $book, UpdateBookRequest $request): JsonResponse
    {
        $this->authorize('update', $book);

        $this->bookService->updateBook($book, $request->all());
        
        if ($request->file('image')) {
            $this->bookService->storeBookImage($book, $request->file('image'));
        }

        return response()->json(new BookResource($book), Response::HTTP_OK);
    }

    public function destroy(Book $book): JsonResponse
    {
        $this->authorize('delete', $book);

        $book->delete();

        return response()->json(null, Response::HTTP_NO_CONTENT);
    }

}

Después de aplicar todos los principios SOLID, nuestro controlador ha evolucionado de un código monolítico a un diseño limpio y modular.

6. Conclusión

Hemos pasado de un controlador que mezclaba responsabilidades a uno que sigue los principios SOLID, lo que nos deja con un código más limpio, fácil de mantener y preparado para el crecimiento. Este enfoque no solo mejora la calidad del código, sino que también hace que nuestro proyecto sea más escalable y adaptable a cambios futuros.

Si quieres profundizar más en la creación de APIs y explorar arquitecturas avanzadas como Hexagonal o DDD en Laravel, ¡déjame un comentario! Y no olvides que todo el código está disponible en nuestro repositorio de Git Hub , donde podrás ver las implementaciones completas y jugar con ellas.

¡Si este artículo te ha dejado con ganas de más y quieres que desvelemos los secretos de arquitecturas avanzadas como Hexagonal, DDD, y más, en el universo Laravel, no dudes en decírmelo en los comentarios! Estoy listo para sumergirme en esos temas y ayudarte a llevar tus habilidades al siguiente nivel. ¡Tu feedback es oro, y me encantaría ayudarte a explorar todo el potencial de Laravel!


Suscríbete a nuestra Newsletter

Mantente al día con nuestras últimas novedades y accede a recursos gratuitos exclusivos.
* indicates required

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *