Skip to main content

API Layer

Implementation Status: The API controllers are currently stub files and not yet implemented. This documentation describes the planned architecture and patterns that will be used when the controllers are built.
The API layer (SGRH.Api) will provide the HTTP interface to the application, exposing REST endpoints that invoke application use cases. Controllers will be thin orchestrators that handle HTTP concerns while delegating business logic to the Application layer.

Current Implementation

Currently, only one controller is implemented:
  • TestDbController (/api/test-db/ping) - Database connectivity testing
The other controller files exist as empty stubs:
  • ClientesController.cs
  • ReservasController.cs
  • HabitacionesController.cs
  • ServiciosAdicionalesController.cs
  • TarifasTemporadasController.cs
  • ReportesController.cs
  • AuditoriaController.cs

Project Structure

SGRH.Api/
├── Controllers/
│   ├── ReservasController.cs
│   ├── HabitacionesController.cs
│   ├── ClientesController.cs
│   ├── ServiciosAdicionalesController.cs
│   ├── TarifasTemporadasController.cs
│   ├── ReportesController.cs
│   ├── AuditoriaController.cs
│   └── TestDbController.cs
├── Program.cs                     # Application entry point
├── appsettings.json              # Configuration
└── appsettings.Development.json

Program.cs Configuration

The application bootstraps services and middleware in Program.cs:
using Microsoft.EntityFrameworkCore;
using SGRH.Persistence.Context;

namespace SGRH.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container
            builder.Services.AddControllers();
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Database configuration
            builder.Services.AddDbContext<SGRHDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

            var app = builder.Build();

            // HTTPS redirection (production only)
            if (!app.Environment.IsDevelopment())
            {
                app.UseHttpsRedirection();
            }

            // Swagger (development only)
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseAuthorization();
            app.MapControllers();

            app.Run();
        }
    }
}
The current Program.cs shows basic setup. In a complete implementation, this would also call AddInfrastructure() and AddApplication() extension methods.

Controller Pattern

Base Controller Structure

Controllers follow ASP.NET Core conventions with attribute routing:
[ApiController]
[Route("api/[controller]")]
public class ReservasController : ControllerBase
{
    private readonly IMediator _mediator; // or direct service injection
    private readonly ILogger<ReservasController> _logger;

    public ReservasController(IMediator mediator, ILogger<ReservasController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    // Endpoints...
}

HTTP Method Mapping

/// <summary>
/// Get all reservations with optional filters
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<ReservaListDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<ReservaListDto>>> GetReservas(
    [FromQuery] ListarReservasQuery query,
    CancellationToken ct)
{
    var result = await _mediator.Send(query, ct);
    return Ok(result);
}

/// <summary>
/// Get reservation by ID with full details
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ReservaDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ReservaDto>> GetReservaById(int id, CancellationToken ct)
{
    var query = new ObtenerReservaPorIdQuery { ReservaId = id };
    var result = await _mediator.Send(query, ct);
    
    if (result == null)
        return NotFound(new { message = $"Reserva {id} no encontrada" });
    
    return Ok(result);
}

Controller Endpoints by Feature

ReservasController

List reservations with filtersQuery Parameters:
  • fechaDesde (DateTime?): Filter by start date
  • fechaHasta (DateTime?): Filter by end date
  • estado (EstadoReserva?): Filter by status
  • clienteId (int?): Filter by client
  • pageNumber (int): Page number (default: 1)
  • pageSize (int): Items per page (default: 20)
Response: PagedResult<ReservaListDto>
Get reservation detailsIncludes full details: rooms, services, pricingResponse: ReservaDto
Create new reservationRequest Body:
{
  "clienteId": 1,
  "fechaEntrada": "2024-12-20",
  "fechaSalida": "2024-12-25",
  "habitacionIds": [101, 102]
}
Response: 201 Created with ReservaDto
Update reservationCan modify dates, add/remove rooms and services (only if Pendiente)
Confirm reservationTransitions state: Pendiente → Confirmada
Check-in guestValidates current date matches entry date, changes room states
Check-out guestFinalizes reservation, frees rooms
Cancel reservationTransitions to Cancelada state

HabitacionesController

List rooms with availabilityQuery Parameters:
  • categoriaId (int?): Filter by category
  • estado (EstadoHabitacion?): Filter by status
  • fechaDesde (DateTime?): Check availability from
  • fechaHasta (DateTime?): Check availability to
Response: List<HabitacionDto>
Get room detailsIncludes current status and history
Create new roomRequest Body:
{
  "categoriaHabitacionId": 1,
  "numeroHabitacion": 101,
  "piso": 1
}
Update room informationCan change category, floor, etc.
Change room statusRequest Body:
{
  "nuevoEstado": "Mantenimiento",
  "motivo": "Reparación de aire acondicionado"
}

ClientesController

List all clientsSupports pagination and search
Get client detailsIncludes contact information and reservation history
Register new clientRequest Body:
{
  "nationalId": "12345678",
  "nombreCliente": "Juan",
  "apellidoCliente": "Pérez",
  "email": "[email protected]",
  "telefono": "+1234567890"
}
Update client informationCannot change NationalId (immutable)
Delete clientOnly if no active reservations

ServiciosAdicionalesController

List available servicesCan filter by season availability
Create new serviceRequest Body:
{
  "nombreServicio": "Spa",
  "tipoServicio": "Wellness"
}
Update service
Delete service

TarifasTemporadasController

List rates by season and category
Create new rateRequest Body:
{
  "categoriaHabitacionId": 1,
  "temporadaId": 2,
  "precio": 150.00
}
List seasons
Create new seasonRequest Body:
{
  "nombreTemporada": "Verano 2024",
  "fechaInicio": "2024-06-01",
  "fechaFin": "2024-08-31"
}

ReportesController

Occupancy reportQuery Parameters:
  • fechaInicio (DateTime)
  • fechaFin (DateTime)
Returns occupancy rates by room and period
Revenue reportBreakdown by rooms and services
Reservation analyticsStatistics and trends

AuditoriaController

Query audit logsQuery Parameters:
  • entidad (string): Entity type
  • accion (string): Action performed
  • usuarioId (int?): User filter
  • fechaDesde (DateTime?)
  • fechaHasta (DateTime?)

Error Handling

Global Exception Handler

public class ExceptionHandlerMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (NotFoundException ex)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (ValidationException ex)
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (BusinessRuleViolationException ex)
        {
            context.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (ConflictException ex)
        {
            context.Response.StatusCode = StatusCodes.Status409Conflict;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (Exception ex)
        {
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
        }
    }
}

HTTP Status Code Mapping

Domain ExceptionHTTP StatusUsage
NotFoundException404 Not FoundEntity doesn’t exist
ValidationException400 Bad RequestInvalid input data
BusinessRuleViolationException422 Unprocessable EntityBusiness rule violation
ConflictException409 ConflictDuplicate or conflicting state
Generic Exception500 Internal Server ErrorUnexpected errors

Swagger/OpenAPI Configuration

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "SGRH API",
        Version = "v1",
        Description = "Sistema de Gestión de Reservas Hoteleras",
        Contact = new OpenApiContact
        {
            Name = "SGRH Team",
            Email = "[email protected]"
        }
    });

    // Include XML comments
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath);

    // Add JWT authentication
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT"
    });
});

CORS Configuration

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", builder =>
    {
        builder.WithOrigins("http://localhost:3000", "https://sgrh.com")
               .AllowAnyMethod()
               .AllowAnyHeader()
               .AllowCredentials();
    });
});

// In middleware pipeline
app.UseCors("AllowFrontend");

Authentication & Authorization

[Authorize(Roles = "Admin")]
[HttpPost]
public async Task<IActionResult> CrearHabitacion([FromBody] CrearHabitacionCommand cmd)
{
    // Only admins can create rooms
}

[Authorize]
[HttpGet("mis-reservas")]
public async Task<IActionResult> GetMisReservas()
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    // Return reservations for current user
}

Key Design Decisions

Thin Controllers

Controllers only handle HTTP concerns, delegate to Application layer

RESTful Conventions

Standard HTTP verbs and status codes for predictable API

Centralized Error Handling

Middleware translates domain exceptions to HTTP responses

API Versioning Ready

Route structure supports versioning (/api/v1/…)

Application Layer

See commands and queries invoked by controllers

Domain Layer

Understand entities and exceptions

Infrastructure

Learn about DI configuration

Build docs developers (and LLMs) love