Endpoints y Enrutamiento Básico
En este módulo, exploraremos cómo las Minimal APIs gestionan la funcionalidad de Crear, Leer, Actualizar y Eliminar (CRUD) al mapear los diferentes verbos HTTP, y cómo manejar las entradas de datos (parámetros y cuerpos de solicitud).
2.1. Mapeo de Verbos HTTP (CRUD)
El corazón de una API REST son los verbos HTTP. En Minimal APIs, cada verbo se mapea con una función app.MapX().
| Verbo HTTP | Método Minimal API | Función REST |
|---|---|---|
| GET | app.MapGet() |
Leer recursos |
| POST | app.MapPost() |
Crear un nuevo recurso |
| PUT | app.MapPut() |
Reemplazar completamente un recurso |
| DELETE | app.MapDelete() |
Eliminar un recurso |
| PATCH | app.MapPatch() |
Actualizar parcialmente un recurso |
Ejemplo de CRUD Básico (Lista en Memoria)
Para nuestros ejemplos, utilizaremos una lista estática en memoria para simular una base de datos. Definiremos un Record simple para los ítems.
2.2. Manejo de Parámetros de Ruta y Consulta
Minimal APIs simplifica enormemente la obtención de datos de entrada: el framework automáticamente infiere si un parámetro proviene de la Ruta, la Consulta o el Cuerpo (Body) de la solicitud.
Extracción de valores de la Ruta (/items/{id:int})
Los parámetros definidos en la plantilla de ruta se pasan directamente como argumentos a la función delegada. Se pueden añadir restricciones de tipo para garantizar que el valor sea correcto (ej. :int).
// Obtiene un ítem por su ID de la ruta
app.MapGet("/items/{id:int}", (int id) => { /* ... */ });
Extracción de valores de la cadena de Consulta (/items?page=1)
Si un argumento no está en la ruta y no es un tipo complejo (que se asumiría como cuerpo), se busca en la cadena de consulta (query string).
// Filtra ítems por su estado (ej. /items/filter?status=pendiente)
app.MapGet("/items/filter", (string status) => { /* ... */ });
Inyección automática de servicios
El framework también puede inyectar automáticamente servicios del contenedor DI o tipos especiales de ASP.NET Core (ej. HttpContext, ILogger<T>) en la firma del delegado.
// Inyecta el Logger automáticamente
app.MapGet("/log", (ILogger<Program> logger) =>
{
logger.LogInformation("Endpoint /log accessed.");
return "Log creado";
});
2.3. Gestión de Respuestas HTTP (IResult)
Mientras que devolver un objeto o una cadena de texto resulta en una respuesta 200 OK, a menudo necesitamos devolver códigos de estado HTTP específicos (ej. 201 Created, 404 Not Found, 400 Bad Request).
Aquí es donde entra en juego la interfaz IResult y su clase auxiliar Results.
Método Results |
Código HTTP | Descripción |
|---|---|---|
Results.Ok(data) |
200 OK | Éxito con contenido. |
Results.Created(url, data) |
201 Created | Éxito en la creación de un nuevo recurso. |
Results.NoContent() |
204 No Content | Éxito sin cuerpo de respuesta (típico para PUT/DELETE). |
Results.NotFound() |
404 Not Found | Recurso no encontrado. |
Results.BadRequest() |
400 Bad Request | Error en la solicitud del cliente (ej. validación). |
Results.Problem(detail) |
500 Internal Server Error | Error del servidor. |
Ejemplo usando IResult para el CRUD
En el archivo de código, verás ejemplos de cómo usar IResult para el manejo de casos como «recurso no encontrado» después de un MapGet con ID.
// Usings necesarios.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http.HttpResults;
using System.Collections.Generic;
using System.Linq;
// ====================================================================
// MODELO DE DATOS EN MEMORIA
// Usamos un 'record' para un tipo inmutable y simple.
// ====================================================================
public record Item(int Id, string Nombre, bool Completado, string Categoria);
// Lista estática que simula la Base de Datos.
public static class DataStore
{
public static List<Item> Items = new List<Item>
{
new Item(1, "Comprar leche", false, "Hogar"),
new Item(2, "Programar Módulo 2", true, "Trabajo"),
new Item(3, "Leer documentación .NET", false, "Trabajo")
};
private static int nextId = 4;
public static int GetNextId() => nextId++;
}
// ====================================================================
// APLICACIÓN PRINCIPAL
// ====================================================================
var builder = WebApplication.CreateBuilder(args);
// Añadimos el servicio de Logging (aunque ya está por defecto, lo inyectaremos en un endpoint)
builder.Services.AddLogging();
var app = builder.Build();
// Configuración de Middleware
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// --------------------------------------------------------------------
// MÓDULO 2: ENDPOINTS Y ENRUTAMIENTO BÁSICO
// --------------------------------------------------------------------
// --------------------------------------------------------------------
// 2.1. MAPEO DE VERBOS HTTP Y CRUD
// --------------------------------------------------------------------
// GET: Leer todos los ítems
// Ruta: /items
app.MapGet("/items", () => DataStore.Items)
.WithName("GetAllItems"); // Se usa en Swagger y Results.Created
// GET: Leer un ítem por ID (Con manejo de IResult)
// Ruta: /items/{id}
app.MapGet("/items/{id:int}", (int id) =>
{
var item = DataStore.Items.FirstOrDefault(i => i.Id == id);
// 2.3. Uso de IResult y Results.NotFound()
return item is null
? Results.NotFound($"Item con ID {id} no encontrado.")
: Results.Ok(item); // 2.3. Uso de Results.Ok(data)
})
.WithName("GetItemById");
// POST: Crear un nuevo ítem
// El framework lee automáticamente el cuerpo JSON y lo mapea al 'record' Item.
// Se inyecta un objeto IResult como tipo de retorno para usar Results.Created.
// Ruta: /items
app.MapPost("/items", (Item newItem) =>
{
// Asignar el siguiente ID
var itemToCreate = newItem with { Id = DataStore.GetNextId() };
DataStore.Items.Add(itemToCreate);
// 2.3. Uso de Results.Created(): Devuelve 201 Created
// El primer argumento es la URL donde se puede encontrar el nuevo recurso.
return Results.Created($"/items/{itemToCreate.Id}", itemToCreate);
})
.WithName("CreateItem");
// PUT: Actualizar un ítem existente (Reemplazo total)
// Ruta: /items/{id}
app.MapPut("/items/{id:int}", (int id, Item updatedItem) =>
{
var index = DataStore.Items.FindIndex(i => i.Id == id);
if (index == -1)
{
return Results.NotFound($"Item con ID {id} no encontrado para actualizar.");
}
// Mantener el ID original aunque el cuerpo JSON no lo incluya o sea incorrecto.
DataStore.Items[index] = updatedItem with { Id = id };
// 2.3. Uso de Results.NoContent(): Devuelve 204 No Content
return Results.NoContent();
});
// DELETE: Eliminar un ítem
// Ruta: /items/{id}
app.MapDelete("/items/{id:int}", (int id) =>
{
var count = DataStore.Items.RemoveAll(i => i.Id == id);
if (count == 0)
{
return Results.NotFound($"Item con ID {id} no encontrado para eliminar.");
}
// Devuelve 204 No Content para una eliminación exitosa
return Results.NoContent();
});
// --------------------------------------------------------------------
// 2.2. MANEJO DE PARÁMETROS DE RUTA, CONSULTA Y SERVICIOS
// --------------------------------------------------------------------
// Endpoint de Consulta (Query String) y Logging (Inyección de Servicio)
// Ruta: /search?category=Hogar
app.MapGet("/search", (string category, ILogger<Program> logger) =>
{
// Inyección de ILogger<Program> en la firma del delegado (Inyección de Servicio)
logger.LogInformation($"Buscando ítems en la categoría: {category}");
// Extracción del parámetro 'category' de la Query String
var results = DataStore.Items
.Where(i => i.Categoria.Equals(category, StringComparison.OrdinalIgnoreCase))
.ToList();
return Results.Ok(results);
});
// --------------------------------------------------------------------
// EJECUCIÓN
// --------------------------------------------------------------------
app.Run();