Skip to content

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 HTMLzero 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 with export 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

---
import { ViewTransitions } from 'astro:transitions';
---
<head>
  <ViewTransitions />
</head>

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:

  1. Astro hosts marketing pages anonymously.
  2. <UserMenu client:only="react" /> island fetches /api/me from the .NET API on hydration.
  3. Cookies on .example.com shared between www.example.com (Astro) and api.example.com (.NET) — set Domain=.example.com and SameSite=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

<Header client:load />
<MainContent client:load />
<Footer client:load />

✅ 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 */ }
<Widget client:load />               @* widget calls /api/data *@

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:load everywhere — 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 = false on 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.

Further reading