API .NET para generar Excel con EPPlus: tutorial completo paso a paso

API .NET para generar Excel con EPPlus: tutorial completo paso a paso

Generar archivos Excel desde backend sigue siendo un caso muy habitual en aplicaciones empresariales: reportes de facturación, listados, exportaciones para administración, conciliaciones, etc.
En este tutorial vas a construir una API REST en .NET que genera archivos .xlsx con EPPlus, usando una estructura sencilla y mantenible.

Actualizado: 20 de febrero de 2026.

Qué vas a construir

Al terminar tendrás:

  1. Una API ASP.NET Core que expone endpoints POST para generar reportes Excel.
  2. Una capa de servicio (IExcelService) que encapsula toda la lógica de EPPlus.
  3. Formato de salida en base64 (data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,...) listo para frontend.
  4. Una base para crecer a múltiples plantillas de reporte.

Proyecto de ejemplo en GitHub

Si prefieres partir de una implementación ya hecha, aquí tienes el repositorio:

https://github.com/davidcantonnadales/excel-net-api-demo

Requisitos previos

  1. .NET SDK instalado (en este tutorial usamos net9.0).
  2. Conocimientos básicos de C# y ASP.NET Core.
  3. Editor: Visual Studio, Rider o VS Code.
  4. curl o Postman para probar endpoints.

Aviso importante sobre licencia de EPPlus

Antes de escribir código, este punto es crítico:

  1. El propio equipo de EPPlus indica que desde EPPlus 5 cambiaron de LGPL a Polyform Noncommercial 1.0.0.
  2. En escenarios comerciales/empresariales, necesitas licencia comercial de EPPlus.
  3. En este tutorial se usa EPPlus 6.2.4, por lo que este aviso aplica totalmente.

Resumen práctico:

  1. Proyecto personal o no comercial: puedes usar contexto/licencia no comercial.
  2. Proyecto comercial: compra y configura licencia comercial antes de producción.

Nota de versión:

  1. En EPPlus 6.2.4 se usa ExcelPackage.LicenseContext.
  2. En ramas más nuevas (EPPlus 8+), el proyecto oficial marca ese enfoque como obsoleto y usa nueva API de licencia.

Más abajo te dejo fuentes oficiales para validarlo.

Paso 1: Crear la solución y el proyecto Web API

mkdir ExcelApiDemo
cd ExcelApiDemo
dotnet new sln -n ExcelApiDemo
dotnet new webapi -n ExcelApiDemo.Api
dotnet sln add ExcelApiDemo.Api/ExcelApiDemo.Api.csproj

Paso 2: Instalar paquetes NuGet

cd ExcelApiDemo.Api
dotnet add package EPPlus --version 6.2.4
dotnet add package Newtonsoft.Json --version 13.0.3
dotnet add package Swashbuckle.AspNetCore --version 9.0.3

Si quieres validar el .csproj, debería verse parecido a esto:

<ItemGroup>
  <PackageReference Include="EPPlus" Version="6.2.4" />
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
</ItemGroup>

Paso 3: Definir modelos de entrada genéricos

Crea un archivo Domain/Reports/ExcelReportRequest.cs:

using Newtonsoft.Json;

namespace ExcelApiDemo.Api.Domain.Reports;

public class ReportItem
{
    [JsonProperty("name")]
    public string? Name { get; set; }

    [JsonProperty("quantity")]
    public int Quantity { get; set; }

    [JsonProperty("unitPrice")]
    public decimal UnitPrice { get; set; }
}

public class ExcelReportRequest
{
    [JsonProperty("customerName")]
    public string? CustomerName { get; set; }

    [JsonProperty("reportDate")]
    public string? ReportDate { get; set; }

    [JsonProperty("reportNumber")]
    public string? ReportNumber { get; set; }

    [JsonProperty("items")]
    public List<ReportItem> Items { get; set; } = new();
}

La idea es separar claramente:

  1. Metadatos del reporte (cliente, fecha, número).
  2. Líneas de detalle (items).

Paso 4: Crear el contrato del servicio

Crea Domain/Common/IExcelService.cs:

using ExcelApiDemo.Api.Domain.Reports;

namespace ExcelApiDemo.Api.Domain.Common;

public interface IExcelService
{
    Task<string> ExportReportAsync(ExcelReportRequest data, string sheetName = "Reporte");
}

Este contrato evita que tu controlador conozca detalles de EPPlus.

Paso 5: Implementar el servicio con EPPlus

Crea Services/ExcelService.cs:

using ExcelApiDemo.Api.Domain.Common;
using ExcelApiDemo.Api.Domain.Reports;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System.Drawing;

namespace ExcelApiDemo.Api.Services;

public class ExcelService : IExcelService
{
    public async Task<string> ExportReportAsync(ExcelReportRequest data, string sheetName = "Reporte")
    {
        // Para EPPlus v5-v7:
        // Uso no comercial. En comercial, configura la licencia adecuada.
        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

        using var package = new ExcelPackage();
        package.Workbook.Properties.Author = "ExcelApiDemo";
        package.Workbook.Worksheets.Add(sheetName);

        var ws = package.Workbook.Worksheets[0];
        ws.Cells.Style.Font.Size = 11;
        ws.Cells.Style.Font.Name = "Calibri";

        ws.Cells[1, 1].Value = "REPORTE DE PRODUCTOS";
        ws.Cells[2, 1].Value = "Cliente";
        ws.Cells[2, 2].Value = data.CustomerName;
        ws.Cells[3, 1].Value = "Fecha";
        ws.Cells[3, 2].Value = data.ReportDate;
        ws.Cells[4, 1].Value = "Nº Reporte";
        ws.Cells[4, 2].Value = data.ReportNumber;

        var headers = new List<string> { "Producto", "Cantidad", "Precio Unitario", "Total" };
        var headerRow = 6;
        var col = 1;

        foreach (var h in headers)
        {
            var cell = ws.Cells[headerRow, col];
            cell.Value = h.ToUpperInvariant();
            cell.Style.Font.Bold = true;
            cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
            cell.Style.Fill.BackgroundColor.SetColor(Color.LightBlue);
            cell.Style.Border.BorderAround(ExcelBorderStyle.Thin);
            col++;
        }

        var row = headerRow + 1;
        decimal grandTotal = 0;

        foreach (var item in data.Items.OrderBy(x => x.Name))
        {
            var lineTotal = item.Quantity * item.UnitPrice;
            grandTotal += lineTotal;

            ws.Cells[row, 1].Value = item.Name;
            ws.Cells[row, 2].Value = item.Quantity;
            ws.Cells[row, 3].Value = item.UnitPrice;
            ws.Cells[row, 4].Value = lineTotal;
            row++;
        }

        ws.Cells[row, 3].Value = "TOTAL";
        ws.Cells[row, 3].Style.Font.Bold = true;
        ws.Cells[row, 4].Value = grandTotal;
        ws.Cells[row, 4].Style.Font.Bold = true;

        ws.Cells[1, 1, row, headers.Count].AutoFitColumns();
        ws.Cells[headerRow, 1, row - 1, headers.Count].AutoFilter = true;

        var bytes = await package.GetAsByteArrayAsync();
        return $"data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,{Convert.ToBase64String(bytes)}";
    }
}

Puntos clave del servicio:

  1. Configura licencia de EPPlus al inicio.
  2. Mantén estilos y columnas en una sección clara.
  3. Calcula totales en backend para no depender de frontend.
  4. Devuelve un data URI cuando quieras que el cliente descargue directamente.

Paso 6: Crear el controlador REST

Crea Controllers/ReportsController.cs:

using ExcelApiDemo.Api.Domain.Common;
using ExcelApiDemo.Api.Domain.Reports;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ExcelApiDemo.Api.Controllers;

[ApiController]
[Route("reports")]
public class ReportsController : ControllerBase
{
    private readonly IExcelService _excelService;

    public ReportsController(IExcelService excelService)
    {
        _excelService = excelService;
    }

    [HttpPost("excel")]
    public async Task<IActionResult> GenerateExcel(
        [FromBody][Required] ExcelReportRequest body)
    {
        var fileBase64 = await _excelService.ExportReportAsync(body);
        return Ok(fileBase64);
    }

    [HttpGet("ping")]
    public IActionResult Ping() => Ok(new { ok = true, at = DateTime.UtcNow });
}

Este patrón te da una API limpia:

  1. El controlador valida y orquesta.
  2. El servicio genera el Excel.
  3. El dominio define contratos de entrada/salida.

Paso 7: Registrar servicios en Program.cs

using ExcelApiDemo.Api.Domain.Common;
using ExcelApiDemo.Api.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IExcelService, ExcelService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Paso 8: Ejecutar y probar la API

Compilar:

dotnet restore
dotnet build -c Release

Ejecutar:

dotnet run

Probar health check:

curl http://localhost:5199/reports/ping

Probar generación Excel:

curl -X POST http://localhost:5199/reports/excel \
  -H "Content-Type: application/json" \
  -d '{
    "customerName": "Cliente Demo",
    "reportDate": "2026-02-20",
    "reportNumber": "RPT-001",
    "items": [
      { "name": "Producto A", "quantity": 2, "unitPrice": 10.50 },
      { "name": "Producto B", "quantity": 1, "unitPrice": 25.00 }
    ]
  }'

Recibirás un string base64 largo con prefijo data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,.

Paso 9: Opciones de salida (base64 vs archivo binario)

En este enfoque se devuelve base64 porque simplifica ciertos frontends web.
Pero también puedes devolver archivo directamente:

return File(bytes,
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    "reporte.xlsx");

Cuándo usar cada opción:

  1. Base64: útil para apps SPA que descargan desde JavaScript.
  2. Binario (FileResult): más eficiente en tamaño y memoria para archivos grandes.

Paso 10: Buenas prácticas para producción

  1. Valida DTOs con DataAnnotations y validaciones de negocio.
  2. Añade CancellationToken en servicios y controladores.
  3. Controla tamaño máximo de payload para evitar abusos.
  4. Evita AutoFitColumns en reportes enormes (puede impactar rendimiento).
  5. Versiona endpoints (/v1/reports/excel) cuando el contrato crezca.
  6. Agrega logging estructurado para trazabilidad de exportaciones.
  7. Define una política clara de licencia EPPlus antes de desplegar.

Cómo adaptar esta estructura a tu proyecto

El mapeo recomendado es:

  1. Program.cs: DI, middleware y Swagger.
  2. Controllers/ReportsController.cs: endpoints de exportación.
  3. Services/ExcelService.cs: lógica principal de creación del workbook.
  4. Domain/Common/IExcelService.cs: contrato de servicio.

Para mantenerlo genérico:

  1. Renombrar DTOs y endpoints a términos neutrales (Report, Summary, etc.).
  2. Mantener estructura por capas y contrato de servicio.
  3. Mover reglas específicas de negocio a otra capa, fuera de la generación Excel.

FAQ rápida

¿EPPlus es gratis?

Depende del uso. Desde la versión 5 cambió de licencia y para uso comercial necesitas licencia comercial.

¿Puedo seguir con EPPlus 4 para evitar coste?

No es buena estrategia a largo plazo: te quedas atrás en soporte y mejoras. Mejor decidir licencia correctamente o evaluar alternativas.

¿Alternativas a EPPlus?

ClosedXML o NPOI son opciones comunes, pero cambia API, rendimiento y capacidades. Evalúa según tus necesidades de formato y licencia.

Fuentes oficiales sobre licencia EPPlus

  1. EPPlus Licensing (sitio oficial): https://epplussoftware.com/en/LicenseOverview
  2. NuGet EPPlus (README y metadatos de licencia): https://www.nuget.org/packages/EPPlus
  3. Wiki oficial EPPlus – Getting Started (notas de licencia): https://github.com/EPPlusSoftware/EPPlus/wiki/Getting-Started

Conclusión

Con esta base puedes crear una API .NET robusta para generar Excel y escalar a múltiples reportes, sin acoplarla a una lógica de negocio concreta.
El punto no negociable es la licencia: deja decidido desde el inicio si tu escenario es no comercial o comercial para evitar problemas legales al pasar a producción.

Repositorio de referencia: https://github.com/davidcantonnadales/excel-net-api-demo

Share this content:

Comentarios

Aún no hay comentarios. ¿Por qué no comienzas el debate?

Deja una respuesta

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.