Astro is not just a static site generator — it has powerful server-side capabilities including API endpoints, middleware, and server-side rendering. Here's what most developers overlook.
API Endpoints
Astro lets you create API routes that return JSON, XML, or any response:
// src/pages/api/posts.json.js
export async function GET({ request }) {
const url = new URL(request.url)
const tag = url.searchParams.get("tag")
const posts = await db.post.findMany({
where: tag ? { tags: { has: tag } } : undefined,
orderBy: { publishedAt: "desc" }
})
return new Response(JSON.stringify(posts), {
headers: { "Content-Type": "application/json" }
})
}
export async function POST({ request }) {
const body = await request.json()
const post = await db.post.create({ data: body })
return new Response(JSON.stringify(post), { status: 201 })
}
Server-Side Rendering (SSR)
With an adapter, Astro pages become server-rendered:
// astro.config.mjs
import { defineConfig } from "astro/config"
import node from "@astrojs/node"
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" })
})
---
// src/pages/dashboard.astro
const user = await getUser(Astro.cookies.get("session")?.value)
if (!user) return Astro.redirect("/login")
const stats = await getUserStats(user.id)
---
<html>
<body>
<h1>Welcome, {user.name}</h1>
<div class="stats">
<p>Posts: {stats.postCount}</p>
<p>Views: {stats.totalViews}</p>
</div>
</body>
</html>
Middleware
Astro middleware runs before every request:
// src/middleware.js
import { defineMiddleware, sequence } from "astro:middleware"
const auth = defineMiddleware(async (context, next) => {
const token = context.cookies.get("session")?.value
if (token) {
context.locals.user = await verifyToken(token)
}
return next()
})
const logging = defineMiddleware(async (context, next) => {
const start = Date.now()
const response = await next()
console.log(`${context.url.pathname} - ${Date.now() - start}ms`)
return response
})
export const onRequest = sequence(auth, logging)
Content Collections API
Astro's content collections provide a type-safe way to query local content:
// src/content/config.ts
import { defineCollection, z } from "astro:content"
const blog = defineCollection({
type: "content",
schema: z.object({
title: z.string(),
date: z.date(),
tags: z.array(z.string()),
draft: z.boolean().default(false)
})
})
export const collections = { blog }
---
// src/pages/blog/index.astro
import { getCollection } from "astro:content"
const posts = await getCollection("blog", ({ data }) => !data.draft)
const sorted = posts.sort((a, b) => b.data.date - a.data.date)
---
<ul>
{sorted.map(post => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString()}</time>
</li>
))}
</ul>
Server Islands (Astro 4+)
Mix static and dynamic content on the same page:
---
// Static shell renders at build time
const staticContent = await getStaticData()
---
<html>
<body>
<h1>{staticContent.title}</h1>
<!-- This component renders on the server at request time -->
<UserGreeting server:defer>
<p slot="fallback">Loading...</p>
</UserGreeting>
<StaticFooter />
</body>
</html>
Key Takeaways
-
API endpoints in
src/pages/api/for REST APIs - SSR mode with adapters for Node, Deno, Cloudflare, Vercel
- Middleware for auth, logging, rate limiting
- Content Collections for type-safe local content queries
- Server Islands for mixing static and dynamic content
- Island Architecture — ship zero JS by default
Explore Astro docs for the complete reference.
Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.
Top comments (0)