[{"data":1,"prerenderedAt":858},["ShallowReactive",2],{"navigation:es":3,"blog-page:es":4,"blogs:es":16},[],{"id":5,"title":6,"body":7,"description":8,"extension":9,"links":7,"meta":10,"navigation":11,"path":12,"seo":13,"stem":14,"__hash__":15},"pages\u002Fes\u002Fblog.yml","Últimos Artículos",null,"Algunos de mis blogs sobre desarrollo e industria tech.","yml",{},true,"\u002Fes\u002Fblog",{"title":6,"description":8},"es\u002Fblog","nc0q9OUXavPTSuh44WQi5Q5sRHG-EfgCkbVz9hwPtjg",[17],{"id":18,"title":19,"author":20,"body":25,"date":848,"description":849,"extension":850,"image":851,"meta":852,"minRead":853,"navigation":11,"path":854,"seo":855,"stem":856,"__hash__":857},"blog\u002Fes\u002Fblog\u002Fclean-arch-template-blog.md","De Boilerplate a Production: Construyendo My Clean Architecture .NET API Template",{"name":21,"avatar":22},"Rafael García González",{"src":23,"alt":24},"https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F158175139?v=4","Mi foto de perfil",{"type":26,"value":27,"toc":825},"minimark",[28,33,37,42,45,48,52,71,92,207,211,216,219,254,257,264,268,274,286,303,308,316,319,324,333,341,343,348,357,369,373,386,388,394,403,411,415,422,424,429,434,438,442,446,453,467,472,500,505,515,523,533,538,552,557,574,576,583,588,596,601,622,628,647,654,656,660,668,672,715,719,744,746,750,777,779,783,800,804,810,814,821],[29,30,32],"h1",{"id":31},"de-boilerplate-a-producción-construyendo-mi-clean-architecture-net-api-template","De Boilerplate a Producción: Construyendo mi Clean Architecture .NET API Template",[34,35,36],"p",{},"Crear un API lista para producción no debería significar empezar desde 0 siempre. En este post, explicaré el diseño y estructura de mi API template de Clean Architecture .NET, qué me motivó a hacerlo, y cómo usarlo para tener una implementación sin sacrificar tiempo y mantenibilidad.",[38,39,41],"h2",{"id":40},"por-qué-hice-este-template","¿Por qué hice este Template?",[34,43,44],{},"Al iniciar un proyecto en .NET, suele perderse mucho tiempo en la configuración inicial y en la instalación de dependencias esenciales para que la aplicación funcione correctamente. Esto incluye aspectos como la configuración de la conexión a la base de datos, la documentación con Swagger, las abstracciones de entidades y servicios, entre otros.",[34,46,47],{},"Por esa razón, decidí crear este template con lo indispensable para comenzar rápidamente el desarrollo de una API REST siguiendo los principios de Clean Architecture.",[38,49,51],{"id":50},"qué-incluye","¿Qué incluye?",[34,53,54,55,59,60,59,63,66,67,70],{},"Este template reúne los bloques fundamentales necesarios para iniciar una API .NET lista para producción, con una estructura limpia y mantenible. Incluye una arquitectura por capas con una separación clara de responsabilidades entre ",[56,57,58],"strong",{},"Domain",", ",[56,61,62],{},"Application",[56,64,65],{},"Infrastructure"," y ",[56,68,69],{},"Presentation.Api",".",[34,72,73,74,77,78,84,85,66,88,91],{},"También implementa ",[56,75,76],{},"CQRS"," para organizar commands y queries de una forma más escalable, junto con un enfoque unificado para el manejo de resultados y errores, lo que permite respuestas consistentes en toda la aplicación. Además, incorpora validación con ",[79,80,81],"em",{},[56,82,83],{},"FluentValidation",", inyección de dependencias mediante métodos de extensión, funcionalidades de autenticación y autorización, envío de correos con plantillas, y persistencia con ",[56,86,87],{},"EF Core",[56,89,90],{},"PostgreSQL",". Además viene con el Dokerfile y Docker compose pre-configurado, listo para desplegar.",[93,94,95,108],"table",{},[96,97,98],"thead",{},[99,100,101,105],"tr",{},[102,103,104],"th",{},"Área",[102,106,107],{},"Qué incluye",[109,110,111,120,127,135,143,151,159,167,175,183,191,199],"tbody",{},[99,112,113,117],{},[114,115,116],"td",{},"Arquitectura",[114,118,119],{},"Clean Architecture con separación clara entre Domain, Application, Infrastructure y Presentation.Api",[99,121,122,124],{},[114,123,76],{},[114,125,126],{},"Commands y Queries con handlers y abstracciones de sender",[99,128,129,132],{},[114,130,131],{},"Manejo de resultados",[114,133,134],{},"Respuestas unificadas para éxito, error, no autorizado, conflicto y no encontrado",[99,136,137,140],{},[114,138,139],{},"Validación",[114,141,142],{},"FluentValidation integrado en la capa de aplicación",[99,144,145,148],{},[114,146,147],{},"Inyección de dependencias",[114,149,150],{},"Configuración limpia mediante métodos de extensión como AddApi, AddApplication, AddDatabase y AddEmailService",[99,152,153,156],{},[114,154,155],{},"Autenticación",[114,157,158],{},"Registro, inicio de sesión, refresh token, generación de JWT y verificación de correo",[99,160,161,164],{},[114,162,163],{},"Autorización",[114,165,166],{},"Control de acceso basado en policies y permisos",[99,168,169,172],{},[114,170,171],{},"Usuarios",[114,173,174],{},"Búsqueda con paginación, ordenamiento y obtención por ID con relaciones",[99,176,177,180],{},[114,178,179],{},"Seguridad",[114,181,182],{},"Funcionalidades de gestión de roles y policies",[99,184,185,188],{},[114,186,187],{},"Correo",[114,189,190],{},"Servicio de correo SMTP con soporte para renderizado de plantillas",[99,192,193,196],{},[114,194,195],{},"Persistencia",[114,197,198],{},"EF Core con PostgreSQL, inicialización de base de datos y seeding",[99,200,201,204],{},[114,202,203],{},"Despliegue",[114,205,206],{},"Dokerfile y Docker compose para despliegues más sencillos",[38,208,210],{"id":209},"project-structure","Project structure",[212,213,215],"h3",{"id":214},"solution-layout","Solution layout",[34,217,218],{},"Este repositorio contiene los siguientes proyectos, a nivel general:",[220,221,222,229,234,239,244],"ul",{},[223,224,225],"li",{},[226,227,228],"code",{},"CleanArchTemplate.Domain",[223,230,231],{},[226,232,233],{},"CleanArchTemplate.Application",[223,235,236],{},[226,237,238],{},"CleanArchTemplate.Infrastructure",[223,240,241],{},[226,242,243],{},"CleanArchTemplate.SharedKernel",[223,245,246,249,250,253],{},[226,247,248],{},"CleanArchTemplate.Presentation.Api"," (",[56,251,252],{},"startup project",")",[34,255,256],{},"Y el archivo solution:",[220,258,259],{},[223,260,261],{},[226,262,263],{},"CleanArchTemplate.sln",[38,265,267],{"id":266},"estructura-del-proyecto-donde-poner-que","Estructura del proyecto (donde poner que)",[269,270,272],"h4",{"id":271},"cleanarchtemplatedomain",[226,273,228],{},[34,275,276,279,280,283],{},[56,277,278],{},"Qué es:"," El núcleo del sistema.",[281,282],"br",{},[56,284,285],{},"Qué va aquí:",[220,287,288,291,294,297,300],{},[223,289,290],{},"Entities \u002F Aggregates",[223,292,293],{},"Value Objects",[223,295,296],{},"Domain Events",[223,298,299],{},"Domain services (pura lógica de negocio)",[223,301,302],{},"Business rules\u002Finvariants",[34,304,305],{},[56,306,307],{},"Notas:",[220,309,310,313],{},[223,311,312],{},"Evita dependencias de ASP.NET Core, EF Core o cualquier librería externa que interactúe con el mundo exterior.",[223,314,315],{},"Este proyecto debe ser el más estable a lo largo del tiempo.",[317,318],"hr",{},[269,320,322],{"id":321},"cleanarchtemplateapplication",[226,323,233],{},[34,325,326,328,329,331],{},[56,327,278],{}," Casos de uso y orquestación de la aplicación.",[281,330],{},[56,332,285],{},[220,334,335,338],{},[223,336,337],{},"Casos de uso \u002F Servicios de la aplicación (commands\u002Fqueries - CQRS)",[223,339,340],{},"Validaciones y flujos del negocio que interactúan con los objetos del dominio",[317,342],{},[269,344,346],{"id":345},"cleanarchtemplateinfrastructure",[226,347,238],{},[34,349,350,352,353,355],{},[56,351,278],{}," Detalles de implementación e integraciones.",[281,354],{},[56,356,285],{},[220,358,359,366],{},[223,360,361,362,365],{},"EF Core ",[226,363,364],{},"DbContext",", mappings, configurations",[223,367,368],{},"Clientes de servicios externos (message brokers, file storage, etc.)",[34,370,371],{},[56,372,307],{},[220,374,375,383],{},[223,376,377,378,380,381,70],{},"Esta capa depende de ",[226,379,62],{}," y ",[226,382,58],{},[223,384,385],{},"Aquí es donde viven las preocupaciones del “mundo real”.",[317,387],{},[269,389,391,393],{"id":390},"cleanarchtemplatepresentationapi-startup-project",[226,392,248],{}," (Startup Project)",[34,395,396,398,399,401],{},[56,397,278],{}," La capa de entrada HTTP de la API (ASP.NET Core).",[281,400],{},[56,402,285],{},[220,404,405,408],{},[223,406,407],{},"Controladores",[223,409,410],{},"Configuración de la API (DI wiring, middleware, Swagger, auth, etc.)",[34,412,413],{},[56,414,307],{},[220,416,417],{},[223,418,419,420,70],{},"Este proyecto es el punto de entrada de la aplicación y es el ",[56,421,252],{},[317,423],{},[269,425,427],{"id":426},"cleanarchtemplatesharedkernel",[226,428,243],{},[34,430,431,433],{},[56,432,278],{}," Bloques compartidos y transversales utilizados entre capas.",[38,435,437],{"id":436},"cómo-usarlo","¿Cómo usarlo?",[212,439,441],{"id":440},"correr-el-proyecto","Correr el proyecto",[269,443,445],{"id":444},"opción-a-correr-con-docker-compose-recomendado","Opción A — Correr con Docker Compose (Recomendado)",[34,447,448,449,452],{},"Este repositorio incluye un ",[226,450,451],{},"compose.yaml"," que inicia:",[220,454,455,461],{},[223,456,457,458,253],{},"API container (expone el ",[56,459,460],{},"puerto 8080",[223,462,463,464,253],{},"PostgreSQL container (expone el ",[56,465,466],{},"puerto 5432",[34,468,469],{},[56,470,471],{},"Start:",[473,474,479],"pre",{"className":475,"code":476,"language":477,"meta":478,"style":478},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","docker compose up --build\n","bash","",[226,480,481],{"__ignoreMap":478},[482,483,486,490,494,497],"span",{"class":484,"line":485},"line",1,[482,487,489],{"class":488},"sBMFI","docker",[482,491,493],{"class":492},"sfazB"," compose",[482,495,496],{"class":492}," up",[482,498,499],{"class":492}," --build\n",[34,501,502],{},[56,503,504],{},"API URL:",[220,506,507],{},[223,508,509],{},[510,511,512],"a",{"href":512,"rel":513},"http:\u002F\u002Flocalhost:8080",[514],"nofollow",[34,516,517],{},[56,518,519,520,522],{},"Base de datos (de ",[226,521,451],{},"):",[524,525,526],"blockquote",{},[34,527,528,529,532],{},"Nota: dentro de la red de Docker, la API usa ",[226,530,531],{},"Host=db"," en su cadena de conexión.",[34,534,535],{},[56,536,537],{},"Stop:",[473,539,541],{"className":475,"code":540,"language":477,"meta":478,"style":478},"docker compose down\n",[226,542,543],{"__ignoreMap":478},[482,544,545,547,549],{"class":484,"line":485},[482,546,489],{"class":488},[482,548,493],{"class":492},[482,550,551],{"class":492}," down\n",[34,553,554],{},[56,555,556],{},"Stop + remove volumes (deletes local DB data):",[473,558,560],{"className":475,"code":559,"language":477,"meta":478,"style":478},"docker compose down -v\n",[226,561,562],{"__ignoreMap":478},[482,563,564,566,568,571],{"class":484,"line":485},[482,565,489],{"class":488},[482,567,493],{"class":492},[482,569,570],{"class":492}," down",[482,572,573],{"class":492}," -v\n",[317,575],{},[212,577,579,580],{"id":578},"opción-b-correr-condotnet-run","Opción B — Correr con ",[226,581,582],{},"dotnet run",[34,584,585],{},[56,586,587],{},"Prerequisitos:",[220,589,590,593],{},[223,591,592],{},".NET SDK instalado",[223,594,595],{},"Una instacia de PostgreSQL (puedes usar Docker para la BD si quieres)",[34,597,598],{},[56,599,600],{},"1) Levantar solo la BD por Docker (opcional):",[473,602,606],{"className":603,"code":604,"language":605,"meta":478,"style":478},"language-shell shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","docker compose up -d db\n","shell",[226,607,608],{"__ignoreMap":478},[482,609,610,612,614,616,619],{"class":484,"line":485},[482,611,489],{"class":488},[482,613,493],{"class":492},[482,615,496],{"class":492},[482,617,618],{"class":492}," -d",[482,620,621],{"class":492}," db\n",[34,623,624,627],{},[56,625,626],{},"2) Levantar la API (startup project):"," Desde la raíz del repo:",[473,629,631],{"className":603,"code":630,"language":605,"meta":478,"style":478},"dotnet run --project CleanArchTemplate.Presentation.Api\n",[226,632,633],{"__ignoreMap":478},[482,634,635,638,641,644],{"class":484,"line":485},[482,636,637],{"class":488},"dotnet",[482,639,640],{"class":492}," run",[482,642,643],{"class":492}," --project",[482,645,646],{"class":492}," CleanArchTemplate.Presentation.Api\n",[34,648,649,650,653],{},"Para las variables de entorno y ",[79,651,652],{},"connection strings",", setealos en tu shell, appsettings o user secrets como convenga.",[317,655],{},[38,657,659],{"id":658},"migraciones-de-entity-framework-core","Migraciones de Entity Framework Core",[34,661,662,663,665,666,70],{},"Las migraciones están en ",[56,664,65],{},", pero debe correrse usando la API como el ",[56,667,252],{},[212,669,671],{"id":670},"agregar-una-migración","Agregar una migración",[473,673,675],{"className":603,"code":674,"language":605,"meta":478,"style":478},"dotnet ef migrations add \u003CNameOfMigration> --startup-project CleanArchTemplate.Presentation.Api --project CleanArchTemplate.Infrastructure\n",[226,676,677],{"__ignoreMap":478},[482,678,679,681,684,687,690,694,697,701,704,707,710,712],{"class":484,"line":485},[482,680,637],{"class":488},[482,682,683],{"class":492}," ef",[482,685,686],{"class":492}," migrations",[482,688,689],{"class":492}," add",[482,691,693],{"class":692},"sMK4o"," \u003C",[482,695,696],{"class":492},"NameOfMigratio",[482,698,700],{"class":699},"sTEyZ","n",[482,702,703],{"class":692},">",[482,705,706],{"class":492}," --startup-project",[482,708,709],{"class":492}," CleanArchTemplate.Presentation.Api",[482,711,643],{"class":492},[482,713,714],{"class":492}," CleanArchTemplate.Infrastructure\n",[212,716,718],{"id":717},"aplicar-una-migración-database-update","Aplicar una migración (database update)",[473,720,722],{"className":603,"code":721,"language":605,"meta":478,"style":478},"dotnet ef database update --startup-project CleanArchTemplate.Presentation.Api --project CleanArchTemplate.Infrastructure\n",[226,723,724],{"__ignoreMap":478},[482,725,726,728,730,733,736,738,740,742],{"class":484,"line":485},[482,727,637],{"class":488},[482,729,683],{"class":492},[482,731,732],{"class":492}," database",[482,734,735],{"class":492}," update",[482,737,706],{"class":492},[482,739,709],{"class":492},[482,741,643],{"class":492},[482,743,714],{"class":492},[317,745],{},[38,747,749],{"id":748},"típico-flujo-de-trabajo-para-agregar-un-nuevo-feature","Típico flujo de trabajo para agregar un nuevo feature",[751,752,753,759,765,771],"ol",{},[223,754,755,758],{},[56,756,757],{},"Domain:"," crear\u002Factualizar entities, value objects",[223,760,761,764],{},[56,762,763],{},"Application:"," agregar casos de uso (command\u002Fquery), interfaces necesarias",[223,766,767,770],{},[56,768,769],{},"Infrastructure:"," implementar persistencia o integraciones (EF Core, external APIs)",[223,772,773,776],{},[56,774,775],{},"Presentation:"," exponer caso de uso vía HTTP endpoint\u002Fcontroller",[317,778],{},[38,780,782],{"id":781},"qué-mejoraría-o-agregaría","¿Qué mejoraría o agregaría?",[220,784,785,788,791,794,797],{},[223,786,787],{},"Migrar a OAuth 2.0",[223,789,790],{},"Agregar servicios de exportación a PDF y Excel",[223,792,793],{},"Agregar AutoMapper",[223,795,796],{},"Agregar implementación de MinIO (S3)",[223,798,799],{},"Agregar implementación de Redis",[38,801,803],{"id":802},"pensamientos-finales","Pensamientos Finales",[34,805,806,807,809],{},"Este template es el resultado de muchas pequeñas decisiones guiadas por necesidades reales de proyectos. Con el tiempo, me di cuenta de que contar con un punto de partida bien estructurado marca una gran diferencia tanto en velocidad como en calidad.",[281,808],{},"\nEn lugar de reinventar la misma base en cada proyecto, prefiero comenzar con algo que ya refleje buenas prácticas de arquitectura y que pueda crecer junto con la aplicación.",[38,811,813],{"id":812},"siéntete-libre-de-contribuir-al-proyecto","Siéntete libre de contribuir al proyecto",[34,815,816],{},[510,817,820],{"href":818,"rel":819},"https:\u002F\u002Fgithub.com\u002Frafarush\u002Fclean-arch-dotnet-api-template",[514],"Repo de Github",[822,823,824],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":478,"searchDepth":826,"depth":826,"links":827},2,[828,829,830,834,835,840,844,845,846,847],{"id":40,"depth":826,"text":41},{"id":50,"depth":826,"text":51},{"id":209,"depth":826,"text":210,"children":831},[832],{"id":214,"depth":833,"text":215},3,{"id":266,"depth":826,"text":267},{"id":436,"depth":826,"text":437,"children":836},[837,838],{"id":440,"depth":833,"text":441},{"id":578,"depth":833,"text":839},"Opción B — Correr con dotnet run",{"id":658,"depth":826,"text":659,"children":841},[842,843],{"id":670,"depth":833,"text":671},{"id":717,"depth":833,"text":718},{"id":748,"depth":826,"text":749},{"id":781,"depth":826,"text":782},{"id":802,"depth":826,"text":803},{"id":812,"depth":826,"text":813},"2025-04-23","Un recorrido práctico por mi template de .NET Clean Architecture, las decisiones arquitectónicas detrás de él y cómo ayuda a entregar APIs mantenibles más rápido.","md","https:\u002F\u002Fopengraph.githubassets.com\u002F47b5e78a0a1b26714c38706c3f354355f8389b8469d117c034556fd60938f2ca\u002Frafarush\u002Fclean-arch-dotnet-api-template",{},8,"\u002Fes\u002Fblog\u002Fclean-arch-template-blog",{"title":19,"description":849},"es\u002Fblog\u002Fclean-arch-template-blog","52ndnacmsqiS9B3VrYhcCkjeu23J-2lrac9DzCbcie0",1775934441097]