Compartir en Twitter
Go to Homepage

CREAR API REST DE NIVEL INDUSTRIAL CON .NET

November 9, 2025

Introducción a la Creación de APIs REST en .NET

En el panorama actual del desarrollo de software, las APIs REST representan un pilar fundamental para la interoperabilidad entre sistemas distribuidos. A partir de noviembre de 2025, con la evolución continua de .NET hacia su versión 9, los desarrolladores buscan enfoques que no solo implementen funcionalidades básicas, sino que incorporen principios de diseño escalable y mantenible desde el inicio. Este tutorial se centra en la construcción de una API REST de nivel industrial utilizando .NET, enfatizando prácticas que se alinean con estándares empleados en entornos corporativos de gran escala.

La relevancia de este enfoque radica en la necesidad de transitar de prototipos rápidos a soluciones productivas que soporten cargas elevadas y evolucionen con los requisitos del negocio. Imagina una aplicación CRUD para la gestión de productos en un comercio electrónico: crear, leer, actualizar y eliminar entradas de manera eficiente, con manejo robusto de errores y pruebas integrales. Aquí, exploraremos desde la arquitectura inicial del servidor backend hasta las validaciones finales, asegurando que cada capa contribuya a la resiliencia del sistema.

Para emprender este viaje, es esencial poseer conocimientos básicos de C# y conceptos de programación orientada a objetos. Familiarízate con el ecosistema .NET, incluyendo herramientas como Visual Studio o Visual Studio Code con la extensión C#. Asumimos que has instalado el SDK de .NET 9, disponible en el sitio oficial de Microsoft, ya que ofrece mejoras en rendimiento y soporte para contenedores nativos, superando las capacidades de versiones anteriores como .NET 6.

Comencemos configurando el entorno de desarrollo. Abre una terminal y ejecuta el comando para crear un nuevo proyecto web API:

dotnet new webapi -n MiApiRestIndustria
cd MiApiRestIndustria

Este comando genera una estructura básica con un controlador de ejemplo y archivos de configuración. El proyecto resultante incluye un archivo Program.cs que sirve como punto de entrada, adaptado para minimal APIs en versiones recientes de .NET. Verifica la estructura inicial con:

MiApiRestIndustria/
├── Controllers/
│   └── WeatherForecastController.cs
├── Program.cs
├── appsettings.json
├── MiApiRestIndustria.csproj
└── ...

Esta base nos permite expandir hacia una arquitectura más sofisticada, incorporando patrones como el de repositorio y servicio para separar preocupaciones.

Arquitectura del Servidor Backend

La arquitectura del servidor backend define la espina dorsal de cualquier API REST. En un contexto industrial, optamos por una estructura en capas que promueva la testabilidad y el mantenimiento. La capa de presentación maneja las solicitudes HTTP a través de controladores; la capa de lógica de negocio encapsula las reglas en servicios; y la capa de datos interactúa con persistencia mediante repositorios.

En .NET, implementamos esto utilizando inyección de dependencias nativa, disponible desde el contenedor integrado en Program.cs. Para nuestra API de gestión de productos, definimos entidades como Producto con propiedades Id, Nombre, Precio y Descripción. Crea un directorio Models y agrega la clase:

namespace MiApiRestIndustria.Models;

public class Producto
{
    public int Id { get; set; }
    public string Nombre { get; set; } = string.Empty;
    public decimal Precio { get; set; }
    public string Descripcion { get; set; } = string.Empty;
}

Esta entidad simple sirve como modelo de dominio. Para persistencia, integramos Entity Framework Core, una ORM recomendada para su eficiencia en consultas LINQ. Instala los paquetes necesarios:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

Configura el DbContext en un nuevo archivo Data/ApplicationDbContext.cs:

using Microsoft.EntityFrameworkCore;
using MiApiRestIndustria.Models;

namespace MiApiRestIndustria.Data;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    public DbSet<Producto> Productos { get; set; }
}

En Program.cs, registra el contexto con una cadena de conexión de appsettings.json:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

Actualiza appsettings.json con:

{
    "ConnectionStrings": {
        "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MiApiDb;Trusted_Connection=true;"
    }
}

Ejecuta la migración inicial para crear la base de datos:

dotnet ef migrations add InitialCreate
dotnet ef database update

Esta configuración establece una arquitectura backend sólida, donde el servidor puede manejar transacciones ACID y escalar horizontalmente mediante clústeres de bases de datos. En entornos de producción, considera agregar caché como Redis para optimizar lecturas frecuentes, pero por ahora, nos enfocamos en lo esencial.

La separación en capas no solo facilita el testing unitario, sino que permite refactorizaciones independientes. Por ejemplo, si cambian los requisitos de negocio, solo ajustas la capa de servicios sin tocar los controladores. Esta modularidad es clave en equipos distribuidos, donde múltiples desarrolladores colaboran simultáneamente.

Implementación de la Lógica del Modelo de API

Una vez establecida la arquitectura, procedemos a implementar la lógica del modelo de API. Esto implica definir operaciones CRUD sobre la entidad Producto, asegurando que cada acción respete principios SOLID, particularmente la responsabilidad única.

Comienza creando un repositorio genérico para abstraer el acceso a datos. En el directorio Repositories, agrega IProductoRepository.cs:

using MiApiRestIndustria.Models;

namespace MiApiRestIndustria.Repositories;

public interface IProductoRepository
{
    Task<IEnumerable<Producto>> GetAllAsync();
    Task<Producto?> GetByIdAsync(int id);
    Task<Producto> CreateAsync(Producto producto);
    Task UpdateAsync(Producto producto);
    Task DeleteAsync(int id);
}

Implementa la interfaz en ProductoRepository.cs:

using Microsoft.EntityFrameworkCore;
using MiApiRestIndustria.Data;
using MiApiRestIndustria.Models;
using MiApiRestIndustria.Repositories;

namespace MiApiRestIndustria.Repositories;

public class ProductoRepository : IProductoRepository
{
    private readonly ApplicationDbContext _context;

    public ProductoRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<Producto>> GetAllAsync()
    {
        return await _context.Productos.ToListAsync();
    }

    public async Task<Producto?> GetByIdAsync(int id)
    {
        return await _context.Productos.FindAsync(id);
    }

    public async Task<Producto> CreateAsync(Producto producto)
    {
        _context.Productos.Add(producto);
        await _context.SaveChangesAsync();
        return producto;
    }

    public async Task UpdateAsync(Producto producto)
    {
        _context.Entry(producto).State = EntityState.Modified;
        await _context.SaveChangesAsync();
    }

    public async Task DeleteAsync(int id)
    {
        var producto = await _context.Productos.FindAsync(id);
        if (producto != null)
        {
            _context.Productos.Remove(producto);
            await _context.SaveChangesAsync();
        }
    }
}

Registra el repositorio en Program.cs:

builder.Services.AddScoped<IProductoRepository, ProductoRepository>();

Esta implementación utiliza async/await para operaciones no bloqueantes, mejorando el throughput en escenarios de alta concurrencia. La lógica del modelo ahora reside en el repositorio, liberando a los controladores de detalles de persistencia.

Para enriquecer la API, considera agregar validaciones en el modelo usando Data Annotations. Actualiza Producto.cs:

using System.ComponentModel.DataAnnotations;

public class Producto
{
    public int Id { get; set; }

    [Required(ErrorMessage = "El nombre es requerido")]
    [StringLength(100, ErrorMessage = "El nombre no puede exceder 100 caracteres")]
    public string Nombre { get; set; } = string.Empty;

    [Range(0.01, double.MaxValue, ErrorMessage = "El precio debe ser mayor a 0")]
    public decimal Precio { get; set; }

    public string Descripcion { get; set; } = string.Empty;
}

Estas anotaciones se integran automáticamente con el pipeline de validación de .NET, asegurando datos consistentes sin código boilerplate adicional.

Refactorización de Rutas

Las rutas en una API REST deben ser intuitivas y RESTful, siguiendo convenciones como /api/productos para colecciones y /api/productos/{id} para instancias. En la estructura inicial de .NET, las rutas se definen en controladores, pero para mayor flexibilidad, refactorizamos hacia un enfoque centralizado.

Crea un directorio Controllers y agrega ProductosController.cs:

using Microsoft.AspNetCore.Mvc;
using MiApiRestIndustria.Models;
using MiApiRestIndustria.Repositories;

namespace MiApiRestIndustria.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductosController : ControllerBase
{
    private readonly IProductoRepository _repository;

    public ProductosController(IProductoRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Producto>>> GetProductos()
    {
        var productos = await _repository.GetAllAsync();
        return Ok(productos);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Producto>> GetProducto(int id)
    {
        var producto = await _repository.GetByIdAsync(id);
        if (producto == null) return NotFound();
        return Ok(producto);
    }

    [HttpPost]
    public async Task<ActionResult<Producto>> CreateProducto(Producto producto)
    {
        var nuevoProducto = await _repository.CreateAsync(producto);
        return CreatedAtAction(nameof(GetProducto), new { id = nuevoProducto.Id }, nuevoProducto);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProducto(int id, Producto producto)
    {
        if (id != producto.Id) return BadRequest();
        await _repository.UpdateAsync(producto);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteProducto(int id)
    {
        await _repository.DeleteAsync(id);
        return NoContent();
    }
}

Esta refactorización elimina rutas duplicadas y centraliza la lógica HTTP. Cada método responde con códigos de estado apropiados: 200 para GET exitoso, 201 para creación, 204 para actualizaciones/eliminaciones, y 404 para no encontrado. Para probar, ejecuta el proyecto con dotnet run y navega a https://localhost:7xxx/swagger, donde Swagger UI genera documentación interactiva automáticamente gracias a Swashbuckle.

En producción, personaliza las rutas para incluir versioning, como /api/v1/productos, agregando [ApiVersion(“1.0”)] y configurando en Program.cs con Microsoft.AspNetCore.Mvc.Versioning. Esto permite evoluciones sin romper clientes existentes.

Creación de Modelos para Datos de Solicitud

Los modelos para datos de solicitud aseguran que las entradas sean validadas y tipadas, previniendo inyecciones y errores en runtime. Distinguimos entre modelos de entrada (DTOs) y entidades de dominio para desacoplar la API externa de la interna.

Crea un directorio Dtos y agrega CreateProductoDto.cs:

using System.ComponentModel.DataAnnotations;

namespace MiApiRestIndustria.Dtos;

public class CreateProductoDto
{
    [Required]
    [StringLength(100)]
    public string Nombre { get; set; } = string.Empty;

    [Range(0.01, double.MaxValue)]
    public decimal Precio { get; set; }

    public string Descripcion { get; set; } = string.Empty;
}

Similarmente, UpdateProductoDto.cs para actualizaciones parciales:

using System.ComponentModel.DataAnnotations;

namespace MiApiRestIndustria.Dtos;

public class UpdateProductoDto
{
    [StringLength(100)]
    public string? Nombre { get; set; }

    [Range(0.01, double.MaxValue)]
    public decimal? Precio { get; set; }

    public string? Descripcion { get; set; }
}

Actualiza el controlador para usar estos DTOs. En CreateProducto:

[HttpPost]
public async Task<ActionResult<Producto>> CreateProducto(CreateProductoDto dto)
{
    var producto = new Producto
    {
        Nombre = dto.Nombre,
        Precio = dto.Precio,
        Descripcion = dto.Descripcion
    };
    var nuevoProducto = await _repository.CreateAsync(producto);
    return CreatedAtAction(nameof(GetProducto), new { id = nuevoProducto.Id }, nuevoProducto);
}

Para Update, mapea propiedades no nulas:

[HttpPut("{id}")]
public async Task<IActionResult> UpdateProducto(int id, UpdateProductoDto dto)
{
    var producto = await _repository.GetByIdAsync(id);
    if (producto == null) return NotFound();

    if (dto.Nombre != null) producto.Nombre = dto.Nombre;
    if (dto.Precio.HasValue) producto.Precio = dto.Precio.Value;
    if (dto.Descripcion != null) producto.Descripcion = dto.Descripcion;

    await _repository.UpdateAsync(producto);
    return NoContent();
}

Este patrón de DTOs reduce el acoplamiento y habilita transformaciones como encriptación de campos sensibles. En .NET 9, aprovecha record types para DTOs inmutables:

public record CreateProductoDto(string Nombre, decimal Precio, string Descripcion);

Con validaciones integradas via [Required], el framework rechaza solicitudes inválidas con 400 Bad Request, incluyendo detalles en el cuerpo de respuesta.

Creación de Interfaces de Servicio

Las interfaces de servicio abstraen la lógica de negocio, permitiendo implementaciones intercambiables y mocking en tests. Para nuestra API, definimos IService en un directorio Services.

Crea IProductoService.cs:

using MiApiRestIndustria.Dtos;
using MiApiRestIndustria.Models;

namespace MiApiRestIndustria.Services;

public interface IProductoService
{
    Task<IEnumerable<Producto>> GetAllAsync();
    Task<Producto?> GetByIdAsync(int id);
    Task<Producto> CreateAsync(CreateProductoDto dto);
    Task UpdateAsync(int id, UpdateProductoDto dto);
    Task DeleteAsync(int id);
}

Esta interfaz declara contratos claros, enfocados en operaciones de alto nivel. Implementa en ProductoService.cs:

using MiApiRestIndustria.Dtos;
using MiApiRestIndustria.Models;
using MiApiRestIndustria.Repositories;
using MiApiRestIndustria.Services;

namespace MiApiRestIndustria.Services;

public class ProductoService : IProductoService
{
    private readonly IProductoRepository _repository;

    public ProductoService(IProductoRepository repository)
    {
        _repository = repository;
    }

    public async Task<IEnumerable<Producto>> GetAllAsync()
    {
        return await _repository.GetAllAsync();
    }

    public async Task<Producto?> GetByIdAsync(int id)
    {
        return await _repository.GetByIdAsync(id);
    }

    public async Task<Producto> CreateAsync(CreateProductoDto dto)
    {
        var producto = new Producto
        {
            Nombre = dto.Nombre,
            Precio = dto.Precio,
            Descripcion = dto.Descripcion
        };
        return await _repository.CreateAsync(producto);
    }

    public async Task UpdateAsync(int id, UpdateProductoDto dto)
    {
        var producto = await _repository.GetByIdAsync(id) ?? throw new KeyNotFoundException($"Producto con ID {id} no encontrado.");
        if (dto.Nombre != null) producto.Nombre = dto.Nombre;
        if (dto.Precio.HasValue) producto.Precio = dto.Precio.Value;
        if (dto.Descripcion != null) producto.Descripcion = dto.Descripcion;
        await _repository.UpdateAsync(producto);
    }

    public async Task DeleteAsync(int id)
    {
        await _repository.DeleteAsync(id);
    }
}

Registra en Program.cs:

builder.Services.AddScoped<IProductoService, ProductoService>();

Ahora, inyecta IProductoService en el controlador, reemplazando el repositorio directo. Esto eleva la abstracción, permitiendo lógica adicional como notificaciones o auditoría en el servicio sin alterar la presentación.

En escenarios complejos, los servicios pueden orquestar múltiples repositorios, implementando transacciones distribuidas con UnitOfWork pattern.

Implementación de Métodos Adicionales

Más allá de CRUD básico, implementamos métodos adicionales para enriquecer la API, como búsqueda por precio o paginación. Estos métodos extienden la utilidad, alineándose con necesidades reales de usuarios.

Agrega al IProductoService:

Task<IEnumerable<Producto>> GetByPrecioRangeAsync(decimal min, decimal max);
Task<IEnumerable<Producto>> GetPagedAsync(int page, int pageSize);

Implementa en ProductoService:

public async Task<IEnumerable<Producto>> GetByPrecioRangeAsync(decimal min, decimal max)
{
    return await _context.Productos.Where(p => p.Precio >= min && p.Precio <= max).ToListAsync();
}

public async Task<IEnumerable<Producto>> GetPagedAsync(int page, int pageSize)
{
    return await _context.Productos
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();
}

Nota: Para el primero, inyecta el DbContext si es necesario, o pasa el repositorio extendido. En el controlador, agrega endpoints:

[HttpGet("precio/{min}/{max}")]
public async Task<ActionResult<IEnumerable<Producto>>> GetByPrecio(decimal min, decimal max)
{
    var productos = await _service.GetByPrecioRangeAsync(min, max);
    return Ok(productos);
}

[HttpGet("paginado/{page}/{pageSize}")]
public async Task<ActionResult<IEnumerable<Producto>>> GetPaged(int page, int pageSize)
{
    var productos = await _service.GetPagedAsync(page, pageSize);
    return Ok(productos);
}

Estos métodos soportan consultas eficientes, utilizando índices en la base de datos para rendimiento óptimo. Para paginación, considera agregar metadatos como total de páginas en una respuesta envolvente.

Optimiza consultas complejas con proyecciones LINQ para evitar over-fetching, reduciendo latencia en redes de alta carga.

Manejo de Errores

El manejo de errores es crucial para APIs robustas, proporcionando feedback claro sin exponer detalles internos. En .NET, usamos middleware para capturar excepciones globalmente.

Crea un archivo Middlewares/GlobalExceptionMiddleware.cs:

using System.Text.Json;

namespace MiApiRestIndustria.Middlewares;

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error no manejado: {Message}", ex.Message);
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            var response = new { Error = "Error interno del servidor", Message = ex.Message };
            var json = JsonSerializer.Serialize(response);
            await context.Response.WriteAsync(json);
        }
    }
}

Registra en Program.cs antes de app.MapControllers():

app.UseMiddleware<GlobalExceptionMiddleware>();

Para errores específicos, lanza excepciones en servicios, como ArgumentException para validaciones. En controladores, usa try-catch para casos puntuales, pero delega lo global al middleware.

Este enfoque centraliza logging con Serilog o el logger nativo, facilitando monitoreo en herramientas como Application Insights. En producción, filtra logs sensibles para compliance con GDPR.

Refactorización del Controlador y Servicios

La refactorización itera sobre el código para mejorar legibilidad y eficiencia. En el controlador, extrae métodos comunes como mapeo de DTOs a un helper. Crea Mappers/ProductoMapper.cs:

using MiApiRestIndustria.Dtos;
using MiApiRestIndustria.Models;

namespace MiApiRestIndustria.Mappers;

public static class ProductoMapper
{
    public static Producto ToEntity(CreateProductoDto dto) => new()
    {
        Nombre = dto.Nombre,
        Precio = dto.Precio,
        Descripcion = dto.Descripcion
    };

    public static void UpdateFromDto(Producto entity, UpdateProductoDto dto)
    {
        if (dto.Nombre != null) entity.Nombre = dto.Nombre;
        if (dto.Precio.HasValue) entity.Precio = dto.Precio.Value;
        if (dto.Descripcion != null) entity.Descripcion = dto.Descripcion;
    }
}

Usa en el servicio para simplificar CreateAsync y UpdateAsync. En el controlador, aplica AutoMapper para mapeos automáticos, instalando el paquete y configurando perfiles.

Para servicios, refactoriza hacia composición: si emerges complejidad, divide en servicios más pequeños inyectados mutuamente. Evalúa cobertura con tools como Coverlet, apuntando a >80% en lógica crítica.

Esta fase de refactorización no es un evento único, sino continua, impulsada por revisiones de código y métricas de rendimiento.

Refactorización del Manejo de Errores

Refactorizar el manejo de errores implica estandarizar respuestas y agregar resiliencia. Define un modelo de error unificado en Models/ErrorResponse.cs:

namespace MiApiRestIndustria.Models;

public class ErrorResponse
{
    public string Tipo { get; set; } = string.Empty;
    public string Mensaje { get; set; } = string.Empty;
    public string? Detalle { get; set; }
}

Actualiza el middleware para usarlo:

var response = new ErrorResponse { Tipo = "InternalServerError", Mensaje = "Error interno del servidor" };
if (ex is KeyNotFoundException)
{
    context.Response.StatusCode = 404;
    response.Tipo = "NotFound";
    response.Mensaje = ex.Message;
}

Para validaciones, habilita ApiBehaviorOptions en Program.cs:

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var response = new ErrorResponse { Tipo = "ValidationError", Mensaje = "Datos de solicitud inválidos" };
        // Agrega detalles de ModelState
        return new BadRequestObjectResult(response);
    };
});

Incorpora Polly para retry y circuit breaker en llamadas externas, aunque no aplicable aquí, prepara para integraciones futuras. Monitorea tasas de error con métricas Prometheus para alertas proactivas.

Pruebas de Solicitudes de API

Las pruebas validan el comportamiento end-to-end, asegurando regresión cero. En .NET, usa xUnit o NUnit con WebApplicationFactory para integración.

Instala paquetes:

dotnet add package Microsoft.AspNetCore.Mvc.Testing
dotnet add package xunit

Crea un proyecto de tests: dotnet new xunit -n MiApiRestIndustria.Tests. En ProductosControllerTests.cs:

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using MiApiRestIndustria;
using Xunit;

public class ProductosControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public ProductosControllerTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Mock DbContext para tests
            });
        });
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetProductos_ReturnsSuccess()
    {
        var response = await _client.GetAsync("/api/productos");
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        Assert.NotEmpty(content);
    }

    [Fact]
    public async Task CreateProducto_InvalidData_ReturnsBadRequest()
    {
        var dto = new { Nombre = "", Precio = -1 };
        var response = await _client.PostAsJsonAsync("/api/productos", dto);
        Assert.Equal(400, (int)response.StatusCode);
    }
}

Ejecuta con dotnet test. Para cobertura, integra reportes HTML. Pruebas de carga con tools como Apache JMeter simulan tráfico, validando escalabilidad.

En CI/CD, integra tests en pipelines Azure DevOps o GitHub Actions para despliegues automatizados.

Conclusiones

Construir una API REST de nivel industrial en .NET demanda atención a detalles arquitectónicos, desde capas separadas hasta manejo prolijo de errores y pruebas exhaustivas. Hemos recorrido un camino que transforma una estructura básica en un sistema resiliente, listo para producción en aplicaciones CRUD reales. Al aplicar estos principios, no solo cumples con estándares actuales, sino que anticipas evoluciones futuras en el ecosistema .NET. Recuerda iterar continuamente, midiendo rendimiento y solicitando feedback de pares. Esta base te empodera para tackling desafíos más complejos, contribuyendo a software que impacta positivamente en usuarios y organizaciones.