Astro with .NET API
Key Points
- Astro is a content-leaning meta-framework: SSG by default, optional SSR, with islands architecture — only the interactive bits hydrate.
- Pairs with .NET as a frontend layer over a .NET 9 API: Astro builds the static / hybrid frontend; .NET hosts the data, auth, business logic.
- Why a .NET shop picks Astro: marketing, docs, content-heavy public pages where a full SPA is overkill but you want a tiny island of interactivity.
- Multi-framework: islands can be React, Vue, Svelte, Solid, Preact — pick per island. Useful when the company already has React widgets.
- Content collections (markdown + Zod-typed frontmatter) make Astro excellent for docs, knowledge bases, blog-shaped content.
- View Transitions API built in — smooth client-side navigation without a SPA framework.
- Compared to Next.js: Astro is content-first, multi-framework, ship-less-JS. Next is app-first, React-only, RSC-heavy.
- Compared to React SPA + .NET (this section's main story): Astro wins for content sites; React SPA wins for app sites.
- Deploy: Azure Static Web Apps (hybrid mode), Cloudflare Pages, Vercel, Netlify, or any Node host.
Concepts (deep dive)
Islands architecture
┌──────────────────────────────────────────┐
│ Static HTML page (server-rendered) │
│ │
│ ┌────────┐ ┌────────────────┐ │
│ │ static │ ←→ │ React island │ │
│ │ HTML │ │ (hydrated) │ │
│ └────────┘ └────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Vue island (also hydrated) │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────┘
Most of the page is plain HTML — zero JS. Only marked components ship JS, and only the JS for those components.
---
// MarketingPage.astro — runs at build/request, never in browser
import Header from '../components/Header.astro';
import SignInButton from '../components/SignInButton.tsx';
---
<Header />
<h1>Hello</h1>
<!-- Only this island ships React + handler -->
<SignInButton client:load />
<footer>© 2026</footer>
client: directives: client:load, client:idle, client:visible, client:media, client:only="react". They control when the JS for an island loads.
Static (SSG) vs Server (SSR) vs Hybrid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'hybrid', // 'static' | 'server' | 'hybrid'
adapter: node({ mode: 'standalone' }),
});
static: every page becomes HTML at build time. No Node at runtime.server: every page rendered on demand. Needs an adapter.hybrid: static by default; opt specific routes into SSR withexport const prerender = false.
For a docs/marketing site over a .NET API, hybrid is the sweet spot.
Calling a .NET API from Astro
Build-time fetch (SSG — runs once during astro build):
---
// src/pages/products.astro
const res = await fetch('https://api.example.com/products', {
headers: { 'X-Api-Key': import.meta.env.BUILD_API_KEY }
});
const products = await res.json();
---
<ul>
{products.map(p => <li>{p.name} — {p.price}</li>)}
</ul>
Request-time fetch (SSR — runs on every request):
---
export const prerender = false;
const products = await fetch(
`${import.meta.env.API_BASE}/products`
).then(r => r.json());
---
<ul>{products.map(p => <li>{p.name}</li>)}</ul>
API route (Astro endpoint that proxies/aggregates):
// src/pages/api/search.ts
export const prerender = false;
export async function GET({ url }) {
const q = url.searchParams.get('q');
const r = await fetch(
`${import.meta.env.API_BASE}/search?q=${encodeURIComponent(q ?? '')}`,
{ headers: { Authorization: `Bearer ${import.meta.env.API_TOKEN}` } });
return new Response(await r.text(), {
headers: { 'Content-Type': 'application/json' }
});
}
The .NET 9 minimal API behind it:
app.MapGet("/products", (IProductRepo repo) => repo.AllAsync());
app.MapGet("/search", (string q, ISearch s) => s.Find(q));
Content collections
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const docs = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
publishedAt: z.date(),
tags: z.array(z.string()).default([]),
}),
});
export const collections = { docs };
---
# src/content/docs/getting-started.md
title: Getting Started
publishedAt: 2026-04-15
tags: [intro, setup]
---
# Welcome
Markdown content goes here.
---
import { getCollection } from 'astro:content';
const all = await getCollection('docs');
---
<ul>{all.map(d => <li><a href={`/docs/${d.slug}`}>{d.data.title}</a></li>)}</ul>
Zod typing means a typo in frontmatter breaks the build — not a runtime page.
View Transitions
Built-in opt-in to the browser View Transitions API: smooth crossfades between page navigations without a SPA. Falls back to plain navigation on older browsers.
Astro vs Next.js
| Axis | Astro | Next.js |
|---|---|---|
| Default output | Static HTML | Streamed RSC + client |
| Frameworks | React, Vue, Svelte, Solid, Preact, Lit | React only |
| Content focus | First-class collections | App router; content possible |
| JS shipped | Zero by default; islands opt in | Everything is React |
| Mental model | Pages with islands | Apps with components |
| Routing | File-based | File-based (app dir) |
| Edge / SSR | Adapter per host | Tight Vercel integration |
| Best at | Marketing, docs, blogs, content | Apps, dashboards |
Both can call a .NET API. The difference is where the framework's gravity pulls you.
Astro vs React SPA + .NET
The rest of this section covers React SPA + .NET. When does Astro win instead?
| Need | Pick |
|---|---|
| Mostly content + occasional widget | Astro |
| Highly interactive app shell | React SPA |
| Marketing site with one signed-in nav island | Astro |
| Internal CRUD admin | React SPA (or Razor / HTMX) |
| Docs site with code samples | Astro + content collections |
| Real-time dashboard | React SPA + SignalR |
| SEO-critical public pages | Astro (or Blazor Static SSR) |
Heuristic: if the page is mostly text and images, Astro. If the page is mostly UI state, SPA.
Auth across Astro and .NET
For a public site with a signed-in nav island:
- Astro hosts marketing pages anonymously.
<UserMenu client:only="react" />island fetches/api/mefrom the .NET API on hydration.- Cookies on
.example.comshared betweenwww.example.com(Astro) andapi.example.com(.NET) — setDomain=.example.comandSameSite=Lax.
For static-only sites, defer auth-aware UI to the island (server-rendered HTML stays public + cacheable).
Deploy targets
| Host | Astro support | Notes |
|---|---|---|
| Azure Static Web Apps | ✅ Hybrid | Pairs with .NET API in same SWA, or external Container App |
| Cloudflare Pages | ✅ Hybrid via Workers | Edge-fast; SSR runs on Workers |
| Vercel | ✅ | First-class adapter |
| Netlify | ✅ | First-class adapter |
| Node host (App Service / Container Apps) | ✅ standalone | Bring your own Node |
For an all-Azure shop: SWA for the Astro frontend, Container Apps or App Service for the .NET API, Front Door tying them together.
Azure Static Web Apps configuration
# .github/workflows/azure-static-web-apps.yml
- uses: Azure/static-web-apps-deploy@v1
with:
app_location: '/'
api_location: ''
output_location: 'dist'
azure_static_web_apps_api_token: ${{ secrets.AZURE_SWA_TOKEN }}
// staticwebapp.config.json
{
"routes": [
{ "route": "/api/*", "rewrite": "https://api.example.com/api/{*}" }
]
}
Performance
Astro's marketing claim — less JS — is real. A hand-tuned React SPA might ship 100 KB. Astro can ship literally 0 KB to a marketing page; an interactive page ships only that island's bundle.
Lighthouse on a content-heavy Astro site routinely scores 95-100 without effort. Same content in Next.js needs careful RSC + dynamic-import discipline.
Code: correct vs wrong
❌ Wrong: SSG-fetching from a behind-firewall API
---
const data = await fetch('https://internal.api/...').then(r => r.json());
---
@* Build server can't reach internal API → build fails *@
✅ Correct: SSR endpoint or public read API for build
---
export const prerender = false;
const data = await fetch(`${import.meta.env.PUBLIC_API}/...`).then(r => r.json());
---
❌ Wrong: hydrating everything
✅ Correct: islands only where interactive
<Header /> @* zero JS *@
<MainContent /> @* zero JS *@
<SignInButton client:load /> @* one tiny island *@
<Footer /> @* zero JS *@
❌ Wrong: leaking server secrets to the client
---
const apiKey = import.meta.env.API_KEY; // OK at build/SSR
---
<Widget client:load apiKey={apiKey} /> // ← bundled into client JS!
✅ Correct: proxy via API route
// src/pages/api/data.ts — runs server-side; key never reaches client
export async function GET() { /* fetch with API_KEY, return safe shape */ }
Design patterns for this topic
Pattern 1 — "SSG content + islands for interactivity"
- Intent: ship near-zero JS to public pages; hydrate only the dynamic bits.
Pattern 2 — "Astro API route as proxy to .NET"
- Intent: keep secrets server-side; tighten CORS to one origin.
Pattern 3 — "Content collections for docs/blog"
- Intent: typed markdown beats CMS for engineer-authored content.
Pattern 4 — "Hybrid mode + selective prerender = false"
- Intent: static for marketing, SSR for
/dashboard/me.
Pattern 5 — "Same .NET API for Astro front + React SPA app"
- Intent: one backend serves the public site and the signed-in app.
Pattern 6 — "View Transitions for SPA-like feel without SPA"
- Intent: smooth nav, full HTML, full SEO.
Pros & cons / trade-offs
| Aspect | Pro | Con |
|---|---|---|
| JS payload | Near-zero default | Manual client: discipline required |
| Multi-framework | React + Vue + Svelte coexist | Multiple runtimes if mixed (cost) |
| Content | Excellent collections | Less mature than Next for app-shaped UIs |
| SEO / Lighthouse | Top-tier | Same for any well-built SSR |
| Build times | Fast | Long for huge content (10k+ pages) |
| Hosting | Anywhere | Hybrid mode requires Node-capable host |
When to use / when to avoid
- ✅ Use for marketing sites, docs, blogs, knowledge bases.
- ✅ Use when you have one or two interactive widgets in an otherwise static site.
- ✅ Use when SEO is non-negotiable and a fully-static-with-islands plan fits.
- ✅ Use when consolidating disparate React/Vue widgets onto one shell.
- ❌ Avoid for highly stateful applications (use React SPA, Blazor, or Next.js).
- ❌ Avoid if your team has zero Node toolchain experience and the site is small (Razor Pages may ship faster).
- ❌ Avoid for real-time dashboards (SignalR + SPA fits better).
Interview Q&A
Q1. What is Astro? A meta-framework that ships HTML by default and selectively hydrates "islands" of interactivity. Multi-framework support.
Q2. What is islands architecture? Most of the page is static HTML; only marked components ship and execute JS. Each island is independent.
Q3. SSG vs SSR vs hybrid in Astro? SSG: build-time HTML; SSR: per-request render via adapter; hybrid: static by default, opt out per-route via prerender = false.
Q4. Astro vs Next.js? Astro: content-first, multi-framework, near-zero JS by default. Next: app-first, React-only, RSC-heavy.
Q5. When pick Astro over a React SPA + .NET API? When the site is mostly content with sprinkles of interactivity — marketing, docs, blogs.
Q6. How does Astro call a .NET API? Plain fetch at build (SSG), at request (SSR), or via Astro API routes that proxy. Identical to any Node frontend.
Q7. What are content collections? Markdown/MDX folders with Zod-typed frontmatter. Type errors fail the build.
Q8. View Transitions? Built-in opt-in to the browser's View Transitions API; smooth nav without a SPA.
Q9. How do client: directives differ? client:load (immediate), client:idle (when idle), client:visible (on intersect), client:media (CSS media query), client:only (no SSR).
Q10. Deploy targets? Azure SWA (hybrid), Cloudflare Pages, Vercel, Netlify, any Node host.
Q11. How avoid leaking secrets in islands? Don't pass server-only env vars as props; proxy via Astro API routes.
Q12. Astro + Razor / Blazor / MVC? Coexist via separate origins or path routing at the edge (Front Door / SWA).
Gotchas / common mistakes
- ⚠️
client:loadeverywhere — defeats the point; pick the right directive. - ⚠️ Build-time fetch from internal-only APIs — build server can't reach them.
- ⚠️ Server env vars passed into islands — bundled into client JS.
- ⚠️ Forgetting
prerender = falseon dynamic routes — stale HTML. - ⚠️ Mixing too many UI frameworks — multiple runtimes inflate JS.
- ⚠️ Treating Astro like Next.js — different mental model; islands ≠ RSC.
- ⚠️ CORS — Astro origin and .NET API origin differ; configure carefully or proxy.
- ⚠️ Massive content collections — build times balloon past 10k pages; consider on-demand SSR for archives.