{"id":322,"date":"2026-05-17T07:04:51","date_gmt":"2026-05-17T11:04:51","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=322"},"modified":"2026-05-17T07:13:42","modified_gmt":"2026-05-17T11:13:42","slug":"blazor-web-apps-arquitectura-real-decisiones-y-trade-offs","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/05\/blazor-web-apps-arquitectura-real-decisiones-y-trade-offs\/","title":{"rendered":"Blazor Web Apps: Arquitectura real, decisiones y trade-offs"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Si has seguido esta serie, ya sabes qu\u00e9 son las Blazor Web Apps, dominas los Render Modes y entiendes a la perfecci\u00f3n c\u00f3mo lidiar con el ciclo de vida y el prerendering (puedes repasar el <a href=\"https:\/\/juredev.com\/blog\/2026\/05\/blazor-web-apps-en-profundidad-estado-ciclo-de-vida-y-pitfalls\/\">\u00faltimo art\u00edculo aqu\u00ed<\/a>). Llegados a este punto, tu aplicaci\u00f3n funciona\u2026 pero, \u00bfest\u00e1 realmente lista para producci\u00f3n?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En cuanto intentas estructurar algo serio, aparece el segundo gran susto de la serie:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">\u00abTengo tres proyectos, dos implementaciones del mismo servicio y no s\u00e9 d\u00f3nde va cada cosa.\u00bb<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">En esta entrega vamos a dejar la teor\u00eda a un lado para hablar de <strong>arquitectura real<\/strong>. Vamos a estructurar una aplicaci\u00f3n que usa el modo Interactive Auto combinando lo mejor del servidor y WebAssembly, manteniendo el c\u00f3digo limpio, modular y seguro.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">TL;DR &#8211; Checklist r\u00e1pida<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Si ya tienes experiencia con Blazor y quieres saber si este art\u00edculo te aporta algo, esta es la esencia:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Si tienes l\u00f3gica compartida<\/strong> &#8211;> Crea un proyecto <code>App.Shared<\/code> con modelos e interfaces. Nunca en Server ni en Client.<\/li>\n\n\n\n<li><strong>Si un servicio usa DbContext<\/strong> &#8211;> Reg\u00edstralo solo en el servidor. En el cliente, su gemelo llama a la API.<\/li>\n\n\n\n<li><strong>Si el cliente WASM necesita datos<\/strong> &#8211;> Exp\u00f3n un Minimal API en el servidor. Configura <code>BaseAddress<\/code> en el cliente.<\/li>\n\n\n\n<li><strong>Si necesitas autenticaci\u00f3n<\/strong> &#8211;> Cookie <code>HttpOnly<\/code> en el servidor + <code>PersistentComponentState<\/code> para transferir los claims a WASM.<\/li>\n\n\n\n<li><strong>Si algo no encaja en un componente<\/strong> &#8211;> No lo fuerces. Minimal APIs, Background Services y controladores conviven bajo el mismo techo.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Si alguno de estos puntos te genera dudas, sigue leyendo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"> 1. Organizaci\u00f3n de proyectos: El l\u00edmite entre servidor y cliente<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cuando creas una Blazor Web App con soporte para WebAssembly, la plantilla genera dos proyectos: <code>Server<\/code> y <code>Client<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Debes crear un tercer proyecto (Class Library) llamado <code>App.Shared<\/code>. Aqu\u00ed vivir\u00e1n:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">El error n\u00famero uno es mezclar responsabilidades. Si tienes un componente <code>Analytics.razor<\/code> que corre en modo Auto (primero en servidor, luego en WASM), \u00bfd\u00f3nde colocas los modelos de datos y las interfaces de servicio que usa?<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">La regla de oro: el proyecto <code>Shared<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Debes crear un tercer proyecto (Class Library) llamado <code>App.Shared<\/code>. Aqu\u00ed vivir\u00e1n:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Modelos de dominio (<code>TaskItem<\/code>, <code>UserDTO<\/code>)<\/li>\n\n\n\n<li>Contratos de servicios (<code>ITaskService<\/code>)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Tanto el servidor como el cliente referenciar\u00e1n a <code>Shared<\/code>. As\u00ed garantizas que el cliente WebAssembly no conozca tus configuraciones de Entity Framework, pero comparta exactamente los mismos modelos.<\/p>\n\n\n\n<script type=\"module\">\n  import mermaid from 'https:\/\/cdn.jsdelivr.net\/npm\/mermaid@10\/dist\/mermaid.esm.min.mjs';\n  mermaid.initialize({ \n    startOnLoad: true, \n    theme: 'dark'\n  });\n<\/script>\n<div style=\"display: flex; justify-content: center; align-items: center; width: 100%; margin: 20px 0;\">\n    <pre class=\"mermaid\" style=\"background: transparent; border: none; color: transparent;\">\ngraph TD\n    Shared[\"App.Shared\\n(Modelos e Interfaces)\"]\n    Server[\"App\\n(Servidor)\"]\n    Client[\"App.Client\\n(WebAssembly)\"]\n\n    Server -.->|\"referencia\"| Shared\n    Client -.->|\"referencia\"| Shared\n    Server -->|\"aloja\"| Client\n    <\/pre>\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Regla de oro<\/strong>: Si un archivo lo necesitan tanto el servidor como el cliente, pertenece a <code>Shared<\/code>. Si solo lo necesita uno de los dos, pertenece a ese proyecto y a ning\u00fan otro.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Inyecci\u00f3n de dependencias h\u00edbrida (Servicios Cross-Mode)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Este es el patr\u00f3n m\u00e1s importante de las Blazor Web Apps. Si tu componente usa <code>@inject ITaskService TaskService<\/code> y corre en el servidor, deber\u00eda ir directo a la base de datos (latencia cero). Pero si corre en WebAssembly, \u00a1no puede hacer consultas a SQL! Debe hacer una llamada HTTP a tu API.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La soluci\u00f3n es elegante: <strong>dos implementaciones de la misma interfaz<\/strong>, una por entorno.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En <code>ServerTaskService<\/code> (Proyecto Servidor):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class ServerTaskService : ITaskService\n{\n    \/\/ Inyecta DbContext aqu\u00ed.\n    public async Task&lt;List&lt;TaskItem>> GetTasksAsync() =>\n        await _db.Tasks.ToListAsync();\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En <code>ClientTaskService<\/code> (Proyecto Cliente):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class ClientTaskService : ITaskService\n{\n    \/\/ Inyecta HttpClient aqu\u00ed.\n    public async Task&lt;List&lt;TaskItem>> GetTasksAsync() =>\n        await _http.GetFromJsonAsync&lt;List&lt;TaskItem>>(\"api\/tasks\");\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Al registrar cada uno en su respectivo <code>Program.cs<\/code>, el componente UI ser\u00e1 completamente agn\u00f3stico al entorno. Blazor inyectar\u00e1 el correcto sin que el componente sepa nada:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ En Program.cs del servidor\nbuilder.Services.AddScoped&lt;ITaskService, ServerTaskService>();\n\n\/\/ En Program.cs del cliente\nbuilder.Services.AddScoped&lt;ITaskService, ClientTaskService>();<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Trade-off<\/strong>: Este patr\u00f3n implica mantener dos implementaciones sincronizadas. Si cambias la firma de ITaskService, debes actualizar ambas. Es el precio de la flexibilidad cross-mode.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. El patr\u00f3n Backend-For-Frontend (BFF)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para que el <code>ClientTaskService<\/code> pueda obtener datos, el servidor debe exponer una API. No uses controladores pesados si no los necesitas. Las <strong>Minimal APIs<\/strong> son las mejores amigas de Blazor Web Apps:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ En Program.cs del servidor\napp.MapGet(\"\/api\/tasks\", async (ITaskService taskService) =>\n    await taskService.GetTasksAsync());<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Esto crea una fachada segura (BFF) donde tu servidor act\u00faa de puente exclusivo para tu propio cliente WASM, simplificando el manejo de CORS y seguridad.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La pieza que une todo es la configuraci\u00f3n del <code>HttpClient<\/code> en el <code>Program.cs<\/code> del proyecto cliente:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ En Program.cs del cliente\nbuilder.Services.AddScoped(sp => new HttpClient\n{\n    BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Al usar <code>builder.HostEnvironment.BaseAddress<\/code>, el cliente siempre apunta al servidor que lo origin\u00f3, sin importar si estamos en desarrollo o producci\u00f3n. Sin esta l\u00ednea, el <code>ClientTaskService<\/code> no sabr\u00eda a d\u00f3nde llamar.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Autenticaci\u00f3n: El monstruo final de lo h\u00edbrido<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La autenticaci\u00f3n es donde los proyectos h\u00edbridos suelen romperse. Si usas JWT, \u00bfd\u00f3nde lo guardas en WASM? (Pista: <code>localStorage<\/code> no es seguro). Si usas Cookies en el servidor, \u00bfc\u00f3mo se entera WASM?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La arquitectura moderna en Blazor Web Apps recomienda <strong>Autenticaci\u00f3n por Cookies en el Servidor<\/strong> con <strong>Transferencia de Estado a WASM<\/strong>. Este flujo se divide en tres actos:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Login en SSR puro<\/strong>: El usuario se autentica contra un endpoint tradicional que establece una Cookie <code>HttpOnly<\/code>.<\/li>\n\n\n\n<li><strong>Transferencia de estado<\/strong>: El servidor serializa los claims del usuario en el HTML inicial usando <code>PersistentComponentState<\/code>.<\/li>\n\n\n\n<li><strong>Reconstrucci\u00f3n en WASM<\/strong>: El cliente WebAssembly lee ese estado y \u00abdespierta\u00bb la sesi\u00f3n sin necesidad de llamar a una API externa.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Con esto consigues la seguridad implacable de las cookies gestionadas por el servidor, junto con la velocidad de ejecuci\u00f3n as\u00edncrona de WebAssembly.<\/p>\n\n\n\n<div style=\"display: flex; justify-content: center; align-items: center; width: 100%; margin: 20px 0;\">\n    <pre class=\"mermaid\" style=\"background: transparent; border: none; color: transparent; width: 100%; max-width: 1200px; transform: scale(1.2); transform-origin: top center;\">\n%%{init: { 'sequence': { 'showSequenceNumbers': true } } }%%\nsequenceDiagram\n    participant B as Navegador\n    participant S as Servidor (SSR)\n    participant W as WebAssembly (Cliente)\n\n    Note over B: El usuario inicia sesi\u00f3n\n    activate B\n    B->>S: Login Form (POST)\n    activate S\n    \n    deactivate B\n    S-->>B: Set-Cookie (HttpOnly)\n    Note right of S: Crea la cookie de sesi\u00f3n\n    S->>S: Serializar User a JSON\n    S->>W: Render con PersistentComponentState\n    activate W\n    Note left of W: Recibe el estado persistido\n\n    W->>W: Leer JSON y reconstruir Claims\n    Note over W: La sesi\u00f3n ya est\u00e1 activa<br>sin llamadas de API adicionales\n\n    deactivate S\n    Note over W: Wasm toma el control de la UI\n    deactivate W\n    <\/pre>\n<\/div>\n<\/br>\n\n\n\n<p class=\"wp-block-paragraph\">La pieza clave en el cliente es el <code>PersistentAuthenticationStateProvider<\/code>. Se encarga de \u00abrecoger\u00bb el estado que el servidor dej\u00f3 preparado en el HTML:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class PersistentAuthenticationStateProvider(PersistentComponentState state)\n    : AuthenticationStateProvider\n{\n    public override async Task&lt;AuthenticationState> GetAuthenticationStateAsync()\n    {\n        \/\/ Si no hay estado serializado, devolvemos un usuario an\u00f3nimo\n        if (!state.TryTakeFromJson&lt;UserInfo>(nameof(UserInfo), out var userInfo))\n        {\n            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));\n        }\n\n        var claims = new List&lt;Claim>\n        {\n            new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),\n            new Claim(ClaimTypes.Name, userInfo.Name)\n        };\n\n        return new AuthenticationState(\n            new ClaimsPrincipal(new ClaimsIdentity(claims, \"Cookies\")));\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Regla de oro<\/strong>: Nunca guardes tokens de autenticaci\u00f3n en <code>localStorage<\/code> desde WASM. Si el servidor ya gestion\u00f3 la cookie <code>HttpOnly<\/code>, deja que \u00e9l sea el guardi\u00e1n. WASM solo necesita saber qui\u00e9n es el usuario, no c\u00f3mo autenticarlo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Cu\u00e1ndo NO usar Blazor (Y por qu\u00e9 est\u00e1 bien)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Blazor es incre\u00edble para la UI, pero no todo tiene que ser un componente. Intentar meterlo todo en Razor es uno de los errores m\u00e1s comunes al llegar a producci\u00f3n:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Procesamiento pesado<\/strong>: Si necesitas exportar un Excel de 50MB o procesar im\u00e1genes, usa un Minimal API o un Background Service. El ciclo de vida de un componente Blazor no est\u00e1 dise\u00f1ado para eso.<\/li>\n\n\n\n<li><strong>WebHooks y APIs externas<\/strong>: Para integraciones como Stripe o Twilio, usa controladores est\u00e1ndar o Minimal APIs. Evitas el overhead del ciclo de vida de los componentes y el c\u00f3digo queda mucho m\u00e1s limpio.<\/li>\n\n\n\n<li><strong>Landing pages de marketing<\/strong>: Para la p\u00e1gina de inicio p\u00fablica donde el SEO y el tiempo de carga inicial son cr\u00edticos, un HTML est\u00e1tico o SSR tradicional suele ser m\u00e1s eficiente que cargar el runtime de Blazor.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">En una Blazor Web App conviven Minimal APIs, MVC y SignalR bajo el mismo techo. El mejor arquitecto no es el que usa su herramienta favorita para todo, sino el que sabe cu\u00e1ndo no usarla.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Reglas Pr\u00e1cticas Finales<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para que no tengas que pensar en esto todos los d\u00edas, aqu\u00ed la checklist completa (tambi\u00e9n disponible al inicio del art\u00edculo para referencia r\u00e1pida):<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Si tienes l\u00f3gica compartida<\/strong> &#8211;> Crea un proyecto <code>App.Shared<\/code> con modelos e interfaces. Nunca en Server ni en Client.<\/li>\n\n\n\n<li><strong>Si un servicio usa DbContext<\/strong> &#8211;> Reg\u00edstralo solo en el servidor. En el cliente, su gemelo llama a la API.<\/li>\n\n\n\n<li><strong>Si el cliente WASM necesita datos<\/strong> &#8211;> Exp\u00f3n un Minimal API en el servidor. Configura <code>BaseAddress<\/code> en el cliente.<\/li>\n\n\n\n<li><strong>Si necesitas autenticaci\u00f3n<\/strong> &#8211;> Cookie <code>HttpOnly<\/code> en el servidor + <code>PersistentComponentState<\/code> para transferir los claims a WASM.<\/li>\n\n\n\n<li><strong>Si algo no encaja en un componente <\/strong>&#8211;> No lo fuerces. Minimal APIs, Background Services y controladores conviven bajo el mismo techo.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Con todo esto, ya tienes la arquitectura para que tu aplicaci\u00f3n escale, sea segura y no se convierta en un monolito de componentes. Pero ahora toca el paso final\u2026<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En el pr\u00f3ximo y \u00faltimo art\u00edculo, veremos \u00abDespliegue y Optimizaci\u00f3n\u00bb: c\u00f3mo preparar nuestra aplicaci\u00f3n para el mundo real, configurando CI\/CD y optimizando el tama\u00f1o del payload de WebAssembly para una experiencia de usuario instant\u00e1nea.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">Todos los ejemplos de este art\u00edculo est\u00e1n extra\u00eddos de la Task Manager App, la aplicaci\u00f3n de referencia que acompa\u00f1a esta serie. Si quieres explorar el c\u00f3digo completo, ver c\u00f3mo encajan las piezas o simplemente tener un punto de partida para tus propios experimentos, el repositorio est\u00e1 disponible en GitHub: <a href=\"https:\/\/github.com\/jure-ve\/TaskManagerApp\" target=\"_blank\" rel=\"noreferrer noopener\">github.com\/jure-ve\/TaskManagerApp<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Si has seguido esta serie, ya sabes qu\u00e9 son las Blazor Web Apps, dominas los Render Modes y entiendes a la perfecci\u00f3n c\u00f3mo lidiar con el ciclo de vida y el prerendering (puedes repasar el \u00faltimo art\u00edculo aqu\u00ed). Llegados a este punto, tu aplicaci\u00f3n funciona\u2026 pero, \u00bfest\u00e1 realmente lista para producci\u00f3n? En cuanto intentas estructurar [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[26,11],"class_list":["post-322","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-net","tag-c"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/322","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=322"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/322\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=322"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=322"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=322"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}