[{"data":1,"prerenderedAt":781},["ShallowReactive",2],{"navigation:en":3,"blog-page:en":4,"blogs:en":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\u002Fen\u002Fblog.yml","Latest Articles",null,"Some of my recent thoughts on design, development, and the tech industry.","yml",{},true,"\u002Fen\u002Fblog",{"title":6,"description":8},"en\u002Fblog","thzJ2hJzh7MeCKpO4-s3ECcGGQirFJEnNM_LHYhphj0",[17],{"id":18,"title":19,"author":20,"body":25,"date":771,"description":772,"extension":773,"image":774,"meta":775,"minRead":776,"navigation":11,"path":777,"seo":778,"stem":779,"__hash__":780},"blog\u002Fen\u002Fblog\u002Fclean-arch-template-blog.md","From Boilerplate to Production: Building 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","My profile picture",{"type":26,"value":27,"toc":748},"minimark",[28,32,36,41,44,47,51,70,89,204,208,213,216,251,254,261,265,271,283,300,305,313,316,321,330,338,340,345,354,366,370,383,385,391,400,408,412,419,421,426,431,435,439,443,450,464,469,497,502,512,520,530,535,549,554,571,573,580,585,593,598,609,615,624,627,629,633,641,645,654,658,667,669,673,700,702,706,723,727,733,737,744],[29,30,19],"h1",{"id":31},"from-boilerplate-to-production-building-my-clean-architecture-net-api-template",[33,34,35],"p",{},"Creating a production-ready API should not mean starting from zero every time. In this post, I’ll walk through the design and structure of my Clean Architecture .NET API template, why I built it, and how yo can use it to have an implementation without sacrificing time and maintainability.",[37,38,40],"h2",{"id":39},"why-i-built-this-template","Why I built this template",[33,42,43],{},"When starting a .NET project, a lot of time is often lost in the initial setup and in installing the necessary dependencies to ensure proper functionality. This includes things like configuring the database connection, Swagger documentation, entity and service abstractions, and more.",[33,45,46],{},"For that reason, I decided to create this template with everything needed to quickly start developing a REST API following the principles of Clean Architecture.",[37,48,50],{"id":49},"what-the-template-includes","What the template includes",[33,52,53,54,58,59,58,62,65,66,69],{},"This template brings together the core building blocks needed to start a production-ready .NET API with a clean and maintainable structure. It includes a layered architecture with clear separation of responsibilities across ",[55,56,57],"strong",{},"Domain",", ",[55,60,61],{},"Application",[55,63,64],{},"Infrastructure",", and ",[55,67,68],{},"Presentation.Api",".",[33,71,72,73,76,77,80,81,84,85,88],{},"It also implements ",[55,74,75],{},"CQRS"," to organize commands and queries in a more scalable way, along with a unified result and error handling approach for consistent responses across the application. On top of that, it includes validation with ",[55,78,79],{},"FluentValidation",", dependency injection through extension methods, authentication and authorization features, email sending with templates, and persistence with ",[55,82,83],{},"EF Core"," and ",[55,86,87],{},"PostgreSQL",".  In addition, it comes with dockerfile and docker compose pre-configuration, deploy-ready.",[90,91,92,105],"table",{},[93,94,95],"thead",{},[96,97,98,102],"tr",{},[99,100,101],"th",{},"Area",[99,103,104],{},"What it includes",[106,107,108,117,124,132,140,148,156,164,172,180,188,196],"tbody",{},[96,109,110,114],{},[111,112,113],"td",{},"Architecture",[111,115,116],{},"Clean Architecture with clear separation into Domain, Application, Infrastructure, and Presentation.Api",[96,118,119,121],{},[111,120,75],{},[111,122,123],{},"Commands and Queries with handlers and sender abstractions",[96,125,126,129],{},[111,127,128],{},"Result handling",[111,130,131],{},"Unified success, error, unauthorized, conflict, and not found responses",[96,133,134,137],{},[111,135,136],{},"Validation",[111,138,139],{},"FluentValidation integrated into the application layer",[96,141,142,145],{},[111,143,144],{},"Dependency Injection",[111,146,147],{},"Clean wiring through extension methods like AddApi, AddApplication, AddDatabase, and AddEmailService",[96,149,150,153],{},[111,151,152],{},"Authentication",[111,154,155],{},"Sign up, sign in, refresh token, JWT generation, and email verification",[96,157,158,161],{},[111,159,160],{},"Authorization",[111,162,163],{},"Policies and permissions-based access control",[96,165,166,169],{},[111,167,168],{},"Users",[111,170,171],{},"Search with pagination, sorting, and get-by-id with relations",[96,173,174,177],{},[111,175,176],{},"Security",[111,178,179],{},"Role and policy management features",[96,181,182,185],{},[111,183,184],{},"Email",[111,186,187],{},"SMTP email service with template rendering support",[96,189,190,193],{},[111,191,192],{},"Persistence",[111,194,195],{},"EF Core with PostgreSQL, database initialization, and seeding",[96,197,198,201],{},[111,199,200],{},"Deployment",[111,202,203],{},"Dockerfile and Docker Compose configured for faster deployments",[37,205,207],{"id":206},"project-structure","Project structure",[209,210,212],"h3",{"id":211},"solution-layout","Solution layout",[33,214,215],{},"This repository contains the following projects (high-level):",[217,218,219,226,231,236,241],"ul",{},[220,221,222],"li",{},[223,224,225],"code",{},"CleanArchTemplate.Domain",[220,227,228],{},[223,229,230],{},"CleanArchTemplate.Application",[220,232,233],{},[223,234,235],{},"CleanArchTemplate.Infrastructure",[220,237,238],{},[223,239,240],{},"CleanArchTemplate.SharedKernel",[220,242,243,246,247,250],{},[223,244,245],{},"CleanArchTemplate.Presentation.Api"," (",[55,248,249],{},"startup project",")",[33,252,253],{},"And the solution file:",[217,255,256],{},[220,257,258],{},[223,259,260],{},"CleanArchTemplate.sln",[37,262,264],{"id":263},"project-structure-explained-where-to-put-what","Project structure explained (where to put what)",[266,267,269],"h4",{"id":268},"cleanarchtemplatedomain",[223,270,225],{},[33,272,273,276,277,280],{},[55,274,275],{},"What it is:"," The core of the system.",[278,279],"br",{},[55,281,282],{},"What goes here:",[217,284,285,288,291,294,297],{},[220,286,287],{},"Entities \u002F Aggregates",[220,289,290],{},"Value Objects",[220,292,293],{},"Domain Events",[220,295,296],{},"Domain services (pure business logic)",[220,298,299],{},"Business rules\u002Finvariants",[33,301,302],{},[55,303,304],{},"Notes:",[217,306,307,310],{},[220,308,309],{},"Avoid dependencies on ASP.NET Core, EF Core, or any external libraries that “talk to the outside world”.",[220,311,312],{},"This project should be the most stable over time.",[314,315],"hr",{},[266,317,319],{"id":318},"cleanarchtemplateapplication",[223,320,230],{},[33,322,323,325,326,328],{},[55,324,275],{}," Application use-cases and orchestration.",[278,327],{},[55,329,282],{},[217,331,332,335],{},[220,333,334],{},"Use cases \u002F application services (commands\u002Fqueries - CQRS)",[220,336,337],{},"Validation and business workflows that coordinate domain objects",[314,339],{},[266,341,343],{"id":342},"cleanarchtemplateinfrastructure",[223,344,235],{},[33,346,347,349,350,352],{},[55,348,275],{}," Implementation details and integrations.",[278,351],{},[55,353,282],{},[217,355,356,363],{},[220,357,358,359,362],{},"EF Core ",[223,360,361],{},"DbContext",", mappings, configurations",[220,364,365],{},"External service clients (HTTP clients, message brokers, file storage, etc.)",[33,367,368],{},[55,369,304],{},[217,371,372,380],{},[220,373,374,375,377,378,69],{},"This layer depends on ",[223,376,61],{}," and ",[223,379,57],{},[220,381,382],{},"This is where “real world” concerns live.",[314,384],{},[266,386,388,390],{"id":387},"cleanarchtemplatepresentationapi-startup-project",[223,389,245],{}," (Startup Project)",[33,392,393,395,396,398],{},[55,394,275],{}," The HTTP API boundary (ASP.NET Core).",[278,397],{},[55,399,282],{},[217,401,402,405],{},[220,403,404],{},"Controllers",[220,406,407],{},"API configuration (DI wiring, middleware, Swagger, auth, etc.)",[33,409,410],{},[55,411,304],{},[217,413,414],{},[220,415,416,417,69],{},"This project is the entry point of the application and is the ",[55,418,249],{},[314,420],{},[266,422,424],{"id":423},"cleanarchtemplatesharedkernel",[223,425,240],{},[33,427,428,430],{},[55,429,275],{}," Cross-cutting\u002Fshared building blocks used across layers.",[37,432,434],{"id":433},"how-i-use-it-in-real-projects","How I use it in real projects",[209,436,438],{"id":437},"running-the-project","Running the project",[266,440,442],{"id":441},"option-a-run-with-docker-compose-recommended-for-local-db","Option A — Run with Docker Compose (recommended for local DB)",[33,444,445,446,449],{},"This repo includes a ",[223,447,448],{},"compose.yaml"," that starts:",[217,451,452,458],{},[220,453,454,455,250],{},"API container (exposes ",[55,456,457],{},"port 8080",[220,459,460,461,250],{},"PostgreSQL container (exposes ",[55,462,463],{},"port 5432",[33,465,466],{},[55,467,468],{},"Start:",[470,471,476],"pre",{"className":472,"code":473,"language":474,"meta":475,"style":475},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","docker compose up --build\n","bash","",[223,477,478],{"__ignoreMap":475},[479,480,483,487,491,494],"span",{"class":481,"line":482},"line",1,[479,484,486],{"class":485},"sBMFI","docker",[479,488,490],{"class":489},"sfazB"," compose",[479,492,493],{"class":489}," up",[479,495,496],{"class":489}," --build\n",[33,498,499],{},[55,500,501],{},"API URL:",[217,503,504],{},[220,505,506],{},[507,508,509],"a",{"href":509,"rel":510},"http:\u002F\u002Flocalhost:8080",[511],"nofollow",[33,513,514],{},[55,515,516,517,519],{},"Database (from ",[223,518,448],{},"):",[521,522,523],"blockquote",{},[33,524,525,526,529],{},"Note: inside Docker networking, the API uses ",[223,527,528],{},"Host=db"," in its connection string.",[33,531,532],{},[55,533,534],{},"Stop:",[470,536,538],{"className":472,"code":537,"language":474,"meta":475,"style":475},"docker compose down\n",[223,539,540],{"__ignoreMap":475},[479,541,542,544,546],{"class":481,"line":482},[479,543,486],{"class":485},[479,545,490],{"class":489},[479,547,548],{"class":489}," down\n",[33,550,551],{},[55,552,553],{},"Stop + remove volumes (deletes local DB data):",[470,555,557],{"className":472,"code":556,"language":474,"meta":475,"style":475},"docker compose down -v\n",[223,558,559],{"__ignoreMap":475},[479,560,561,563,565,568],{"class":481,"line":482},[479,562,486],{"class":485},[479,564,490],{"class":489},[479,566,567],{"class":489}," down",[479,569,570],{"class":489}," -v\n",[314,572],{},[209,574,576,577],{"id":575},"option-b-run-withdotnet-run","Option B — Run with ",[223,578,579],{},"dotnet run",[33,581,582],{},[55,583,584],{},"Prerequisites:",[217,586,587,590],{},[220,588,589],{},".NET SDK installed",[220,591,592],{},"A running PostgreSQL instance (you can still use Docker only for the DB if you want)",[33,594,595],{},[55,596,597],{},"1) Start only the database via Docker (optional but common):",[470,599,603],{"className":600,"code":601,"language":602,"meta":475,"style":475},"language-shell shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","docker compose up -d db\n","shell",[223,604,605],{"__ignoreMap":475},[479,606,607],{"class":481,"line":482},[479,608,601],{},[33,610,611,614],{},[55,612,613],{},"2) Run the API (startup project):"," From the repository root:",[470,616,618],{"className":600,"code":617,"language":602,"meta":475,"style":475},"dotnet run --project CleanArchTemplate.Presentation.Api\n",[223,619,620],{"__ignoreMap":475},[479,621,622],{"class":481,"line":482},[479,623,617],{},[33,625,626],{},"The environment variables \u002F connection strings, set them in your shell, appsettings or user secrets as appropriate.",[314,628],{},[37,630,632],{"id":631},"entity-framework-core-migrations","Entity Framework Core migrations",[33,634,635,636,638,639,69],{},"Migrations live in ",[55,637,64],{},", but they must run using the API as the ",[55,640,249],{},[209,642,644],{"id":643},"add-a-migration","Add a migration",[470,646,648],{"className":600,"code":647,"language":602,"meta":475,"style":475},"dotnet ef migrations add \u003CNameOfMigration> --startup-project CleanArchTemplate.Presentation.Api --project CleanArchTemplate.Infrastructure\n",[223,649,650],{"__ignoreMap":475},[479,651,652],{"class":481,"line":482},[479,653,647],{},[209,655,657],{"id":656},"apply-migrations-update-database","Apply migrations (update database)",[470,659,661],{"className":600,"code":660,"language":602,"meta":475,"style":475},"dotnet ef database update --startup-project CleanArchTemplate.Presentation.Api --project CleanArchTemplate.Infrastructure\n",[223,662,663],{"__ignoreMap":475},[479,664,665],{"class":481,"line":482},[479,666,660],{},[314,668],{},[37,670,672],{"id":671},"typical-workflow-for-adding-a-new-feature","Typical workflow for adding a new feature",[674,675,676,682,688,694],"ol",{},[220,677,678,681],{},[55,679,680],{},"Domain:"," create\u002Fupdate entities, value objects",[220,683,684,687],{},[55,685,686],{},"Application:"," add a use case (command\u002Fquery), interfaces needed",[220,689,690,693],{},[55,691,692],{},"Infrastructure:"," implement persistence or integrations (EF Core, external APIs)",[220,695,696,699],{},[55,697,698],{},"Presentation:"," expose the use case via HTTP endpoint\u002Fcontroller and wire DI",[314,701],{},[37,703,705],{"id":704},"what-i-would-improve-and-add-next","What I would improve and add next",[217,707,708,711,714,717,720],{},[220,709,710],{},"Migrate to OAuth 2.0",[220,712,713],{},"Add PDF and Excel exportation",[220,715,716],{},"Add AutoMapper",[220,718,719],{},"Add MinioService implementation",[220,721,722],{},"Add Redis implementation",[37,724,726],{"id":725},"final-thoughts","Final thoughts",[33,728,729,730,732],{},"This template is the result of many small decisions shaped by real project needs. Over time, I realized that having a well-structured starting point makes a big difference in both speed and quality.",[278,731],{},"\nRather than reinventing the same foundation on every project, I prefer to begin with something that already reflects good architectural practices and can grow with the application.",[37,734,736],{"id":735},"feel-free-to-contribute-to-the-project","Feel free to contribute to the project",[33,738,739],{},[507,740,743],{"href":741,"rel":742},"https:\u002F\u002Fgithub.com\u002Frafarush\u002Fclean-arch-dotnet-api-template",[511],"Github Repo",[745,746,747],"style",{},"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 .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}",{"title":475,"searchDepth":749,"depth":749,"links":750},2,[751,752,753,757,758,763,767,768,769,770],{"id":39,"depth":749,"text":40},{"id":49,"depth":749,"text":50},{"id":206,"depth":749,"text":207,"children":754},[755],{"id":211,"depth":756,"text":212},3,{"id":263,"depth":749,"text":264},{"id":433,"depth":749,"text":434,"children":759},[760,761],{"id":437,"depth":756,"text":438},{"id":575,"depth":756,"text":762},"Option B — Run with dotnet run",{"id":631,"depth":749,"text":632,"children":764},[765,766],{"id":643,"depth":756,"text":644},{"id":656,"depth":756,"text":657},{"id":671,"depth":749,"text":672},{"id":704,"depth":749,"text":705},{"id":725,"depth":749,"text":726},{"id":735,"depth":749,"text":736},"2025-04-23","A practical walkthrough of my .NET Clean Architecture template, the architectural decisions behind it, and how it helps ship maintainable APIs faster.","md","https:\u002F\u002Fopengraph.githubassets.com\u002F47b5e78a0a1b26714c38706c3f354355f8389b8469d117c034556fd60938f2ca\u002Frafarush\u002Fclean-arch-dotnet-api-template",{},8,"\u002Fen\u002Fblog\u002Fclean-arch-template-blog",{"title":19,"description":772},"en\u002Fblog\u002Fclean-arch-template-blog","a1cB5mtdbKCl4MGd9AgvdQGAP-pOkuRC8e51vcb-o4o",1775934440363]