So far, everything we’ve placed inside the src/pages folder have been components (.astro, .md) that render HTML.
But, what if we don’t want to return HTML?
Imagine you want to:
- Create an internal REST API for your frontend to consume data.
- Generate a
sitemap.xmlorrobots.txtfile dynamically. - Receive form data via a
POSTrequest. - Generate images on the fly.
For all of this, Astro allows us to create Endpoints (also known as API Routes).
What is an Endpoint in Astro?
An Endpoint is a file inside src/pages that does not export an Astro component, but instead exports functions corresponding to HTTP methods (GET, POST, PUT, DELETE, etc.).
These files must have the .js or .ts extension (not .astro).
Our first Endpoint (GET)
Let’s create a route that returns a greeting in JSON format. Create the file src/pages/api/saludo.ts.
// src/pages/api/saludo.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params, request }) => {
return new Response(
JSON.stringify({
mensaje: "¡Hola desde la API de Astro!",
hora: new Date().toISOString()
}),
{
status: 200,
headers: {
"Content-Type": "application/json"
}
}
);
}
If you now visit http://localhost:4321/api/saludo, you will see the JSON in your browser.
Web Standards: Astro uses the standard Request and Response objects from the Fetch API. If you have worked with Service Workers or Cloudflare Workers, this will be familiar. There are no proprietary req and res objects like in Express.
Endpoint Context
The function we export receives a context object (similar to the global Astro object in .astro components). From here we can extract:
params: The dynamic parameters of the route (e.g.,[id]).request: The standard Request object (contains headers, body, url…).cookies: Utilities for reading and writing cookies.redirect: Function to redirect the user.
Dynamic Endpoints
Just like with pages, we can use brackets to create dynamic API routes.
Imagine we want an API to get product data: src/pages/api/productos/[id].ts.
// src/pages/api/productos/[id].ts
import type { APIRoute } from 'astro';
import { getProductById } from '../../lib/db'; // Let's imagine a DB
export const GET: APIRoute = async ({ params, request }) => {
const id = params.id;
// Search in our "database"
const producto = await getProductById(id);
if (!producto) {
return new Response(JSON.stringify({ error: "No encontrado" }), {
status: 404,
});
}
return new Response(JSON.stringify(producto), {
status: 200,
headers: { "Content-Type": "application/json" }
});
}
Important note about SSG vs SSR
The same thing we saw in the previous chapter happens here:
output: 'static'mode (SSG): You need to export thegetStaticPathsfunction inside the.tsfile to tell Astro which endpoints it should generate when compiling. The result will be static files (producto-1.json,producto-2.json).output: 'server'mode (SSR): You don’t needgetStaticPaths. The endpoint will run in real time when someone makes the request.
Handling POST Requests (Forms and Data)
Endpoints are the perfect place to process forms or receive data from the client. Let’s see how to handle a POST to receive contact data.
// src/pages/api/contacto.ts
import type { APIRoute } from 'astro';
export const POST: APIRoute = async ({ request }) => {
try {
// Read the request body.
// If they send JSON we use request.json()
// If it's an HTML Form we use request.formData()
const body = await request.json();
const { nombre, email } = body;
// Basic validation
if (!nombre || !email) {
return new Response(
JSON.stringify({ message: "Faltan datos" }),
{ status: 400 }
);
}
// Here we would call our email service or database
console.log(`Guardando contacto: ${nombre}`);
return new Response(
JSON.stringify({ message: "¡Recibido con éxito!" }),
{ status: 200 }
);
} catch (error) {
return new Response(
JSON.stringify({ message: "Error del servidor" }),
{ status: 500 }
);
}
}
You can export GET, POST, PUT, DELETE and ALL in the same file to handle different methods on the same route.
Generating other formats (Images, XML)
We are not limited to JSON. We can return whatever we want.
A very common use case is generating a robots.txt dynamically based on whether we are in production or development.
// src/pages/robots.txt.ts
import type { APIRoute } from 'astro';
const getRobotsTxt = (baseUrl: URL) => `
User-agent: *
Allow: /
Sitemap: ${baseUrl.href}sitemap-index.xml
`.trim();
export const GET: APIRoute = ({ site }) => {
return new Response(getRobotsTxt(site), {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
};
