Overview
Astro defaults to output: "static": every page is prerendered at build time. Installing an adapter lets individual routes render on demand with export const prerender = false. The Astro 4 hybrid mode was merged into static in Astro 5, so it no longer exists. An adapter is required for any server-rendered route, and the build fails or warns without one.
Keep output: "static" and opt routes into SSR
Content routes stay prerendered and CDN-cacheable while auth endpoints and dynamic pages run on the server.
// astro.config.mjs
import { defineConfig } from "astro/config";
import vercel from "@astrojs/vercel";
export default defineConfig({ adapter: vercel() });---
export const prerender = false;
---Add the export only where a page needs request data. Avoid output: "server" on a content site; it server-renders everything, which costs more than serving prerendered files. Use server when most routes need auth or user data, and opt individual pages back in with export const prerender = true.
Pick the adapter for your host
Install with npx astro add <name>.
- Node.js:
@astrojs/nodewithmode: "standalone"(a server that starts itself) or"middleware"(for Express or Fastify). - Vercel:
@astrojs/vercel. See vercel. - Netlify:
@astrojs/netlify(the old/functionsentry point is gone). - Cloudflare:
@astrojs/cloudflaretargets Workers; official Pages support was dropped in adapter v13 (Astro 6), so migrate Pages projects to Workers. Read bindings withimport { env } from "cloudflare:workers"and enablenodejs_compatfor Node APIs. See cloudflare.
Read request data in on-demand routes
Astro.request is a standard Request, Astro.cookies reads and writes cookies, and Astro.redirect() redirects. Set response headers at the page level only, because headers are sent before components render. Do not read request headers or cookies in a prerendered page. security.checkOrigin is on by default and rejects cross-origin form POSTs to server-rendered pages.
---
export const prerender = false;
const session = Astro.cookies.get("session");
if (!session) return Astro.redirect("/login");
const user = await getUser(session.value); // call your data layer directly, not a relative fetch
---
<p>Welcome, {user.name}</p>Write endpoints as .ts files in src/pages/
For mutations your own pages call, prefer astro-actions, which adds Zod validation and typed calls.
Export GET, POST, PUT, DELETE, or ALL as APIRoute functions that receive request, cookies, params, and locals. With the default static output, endpoints are prerendered unless they export prerender = false; add it to any endpoint that reads request data.
// src/pages/api/user.ts
export const GET: APIRoute = async ({ cookies }) => {
const token = cookies.get("token")?.value;
if (!token) return new Response("Unauthorized", { status: 401 });
return Response.json(await lookupUser(token));
};Use sessions for per-user state
Astro.session.get, set, destroy, and regenerate (stable since 5.7) keep data server-side behind a cookie. The Node, Cloudflare, and Netlify adapters provide a default storage driver; other adapters need session.driver. Set session: false (7.2+) to drop the default driver and shrink serverless bundles when you do not use sessions.
Keep secrets in astro:env
Declare variables in the config with envField and import them from astro:env/server or astro:env/client. A context: "server", access: "secret" variable is never bundled; secret client variables are not allowed. astro:env does not work in astro.config.mjs; use process.env there.
Cache on-demand responses with route rules
Astro 7 stabilized route caching: configure a cache provider and routeRules, or call Astro.cache.set({ maxAge, swr, tags }) in a route.
import { defineConfig, memoryCache } from "astro/config";
export default defineConfig({
cache: { provider: memoryCache() },
routeRules: { "/blog/[...path]": { maxAge: 300, swr: 60 } },
});Deferred personalized fragments belong in server islands; see astro-islands.