astro-endpoints-api-routes

Endpoints and API Routes in Astro

  • 4 min

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.xml or robots.txt file dynamically.
  • Receive form data via a POST request.
  • 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"
      }
    }
  );
}
Copied!

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" }
  });
}
Copied!

Important note about SSG vs SSR

The same thing we saw in the previous chapter happens here:

  1. output: 'static' mode (SSG): You need to export the getStaticPaths function inside the .ts file to tell Astro which endpoints it should generate when compiling. The result will be static files (producto-1.json, producto-2.json).
  2. output: 'server' mode (SSR): You don’t need getStaticPaths. 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 }
    );
  }
}
Copied!

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',
    },
  });
};
Copied!