Skip to content
Astro interview 4 min read

Advanced & Architecture Questions

Senior Astro interviews move past “what is an island” into how you compose middleware, validate server actions, orchestrate view transitions, and reason about performance budgets across a real deployment. The questions below probe whether you understand Astro’s request lifecycle, its zero-JS-by-default model, and the trade-offs you make when you reach for SSR, edge functions, or client islands. Treat each answer as a chance to show architectural judgement, not just API recall.

How does Astro middleware work, and when would you use it?

Middleware runs on the server for every request before a route renders, letting you intercept, mutate, or short-circuit the response. You export an onRequest function (or compose several with sequence) from src/middleware.ts. It receives a context and a next callback; you can read cookies, write to context.locals, redirect, or return a Response directly to stop rendering.

A common use is authentication or attaching per-request data that any page can read from Astro.locals.

// src/middleware.ts
import { defineMiddleware, sequence } from "astro:middleware";

const auth = defineMiddleware(async (context, next) => {
  const token = context.cookies.get("session")?.value;
  context.locals.user = token ? await lookupUser(token) : null;

  if (context.url.pathname.startsWith("/dashboard") && !context.locals.user) {
    return context.redirect("/login");
  }
  return next();
});

const timing = defineMiddleware(async (context, next) => {
  const start = Date.now();
  const response = await next();
  response.headers.set("Server-Timing", `total;dur=${Date.now() - start}`);
  return response;
});

export const onRequest = sequence(auth, timing);

Tip: Middleware runs only in on-demand (SSR) routes. On statically prerendered pages it executes at build time, so never put per-user auth logic in a prerendered route.

What are Astro Actions and how do they differ from API routes?

Actions are typed server functions you call from the client (or in .astro frontmatter) with automatic input validation, progressive enhancement, and structured error handling. Unlike a raw API route, an action gives you end-to-end type safety, Zod-based validation, and a <form> integration that works without JavaScript.

// src/actions/index.ts
import { defineAction } from "astro:actions";
import { z } from "astro:schema";

export const server = {
  subscribe: defineAction({
    accept: "form",
    input: z.object({ email: z.string().email() }),
    handler: async ({ email }) => {
      await saveSubscriber(email);
      return { ok: true };
    },
  }),
};
---
// src/pages/index.astro
import { actions } from "astro:actions";
---
<form method="POST" action={actions.subscribe}>
  <input name="email" type="email" required />
  <button>Subscribe</button>
</form>

The form posts and re-renders even with JS disabled; with JS, you can call actions.subscribe(formData) and inspect { data, error }. Use an API route instead when you need a public REST/JSON contract for external consumers.

How do View Transitions work in Astro?

The <ClientRouter /> component (formerly <ViewTransitions />) turns full-page navigations into smooth, animated transitions by intercepting links, swapping the DOM, and using the browser’s View Transitions API where available. It enables SPA-like routing while keeping each page server-rendered and zero-JS by default.

---
import { ClientRouter } from "astro:transitions";
---
<head>
  <ClientRouter />
</head>

Add transition:name to pair elements across pages for a morph animation, transition:animate to pick fade/slide, and transition:persist to keep stateful islands (like a playing <video> or audio player) alive across navigations. Listen for lifecycle events such as astro:page-load and astro:after-swap to re-run scripts.

What levers do you have for performance tuning?

LeverWhat it controlsWhen to reach for it
client:loadHydrate island immediatelyCritical interactivity above the fold
client:idleHydrate when main thread is freeImportant but non-urgent widgets
client:visibleHydrate on scroll into viewBelow-the-fold components
client:mediaHydrate at a breakpointMobile-only or desktop-only UI
client:onlySkip SSR, render only on clientComponents that can’t run on the server
<Image />Optimized, responsive imagesAny raster asset
prefetchPreload links on hover/viewportLikely next navigations

The biggest win is simply shipping fewer islands: every client:* directive is JavaScript you pay for. Audit your bundle, prefer static HTML, and push interactivity to the smallest possible component boundary.

How would you architect a large Astro site?

Lead with rendering strategy. Pick output: "static" for content-heavy marketing/docs sites, switch to "server" (with an adapter like Node, Cloudflare, or Vercel) only for routes that genuinely need per-request data, and use export const prerender = true/false to mix the two per route. Model your content with content collections so it stays type-safe and decoupled from the view layer. Centralize cross-cutting concerns (auth, headers, logging) in middleware, expose mutations through actions, and keep UI-framework islands isolated so you can mix React, Svelte, and Vue without coupling.

Warning: Don’t make a whole route SSR just to read one cookie. Prerender the shell and hydrate a small island, or use an edge-cached SSR response, to avoid losing CDN cacheability.

Best Practices

  • Keep islands small and choose the laziest client:* directive that still feels responsive.
  • Use context.locals to pass per-request data from middleware to pages instead of re-fetching.
  • Validate every action input with Zod and return structured errors, not thrown strings.
  • Prefer prerendering; opt into SSR per route with prerender = false only where needed.
  • Use transition:persist for stateful islands so view transitions don’t reset them.
  • Measure with real budgets (LCP, TBT, bundle size) before adding hydration.
Last updated June 14, 2026
Was this helpful?