🇺🇸 EN
Frontend
·13 min read·↗·🇺🇸 EN

Cómo crear tu primera aplicación con Astro

Portada principal del artículo: Cómo crear tu primera aplicación con Astro

Desde hace algunos años, los proyectos web han ido asumiendo cada vez más responsabilidades, lo que se traduce en más y más JavaScript.

Hoy en día, los principales frameworks para SPAs como React, Vue o Angular ofrecen una excelente experiencia de desarrollo (DX) y nos permiten construir aplicaciones complejas, pero tienen algunos puntos débiles:

  • El tamaño del JavaScript que enviamos a los navegadores.

  • Al SEO no le gustan las SPAs tradicionales.

Cada framework intenta solucionar el problema con estrategias de empaquetado, división en módulos (code splitting), carga diferida (lazy loading) y SSR para mitigar el impacto. Sin embargo, el proceso de hidratación suele ser muy pesado para el navegador, por lo que el problema de fondo aún no queda resuelto al 100%.

¿Qué podemos hacer?

La web evoluciona constantemente y existen soluciones pensadas para ayudarnos con el rendimiento y el SEO. Hoy quiero hablarte sobre Astro.

¿Qué es Astro?

Astro es un generador de sitios web estáticos enfocado en el rendimiento y el SEO, que ofrece interactividad mediante sus propios componentes de Astro o utilizando componentes de React, Vue o Svelte.

Astro nos ayuda a alejarnos de sitios web sobrecargados de JavaScript, manteniendo la ventaja de crear sitios escalables y fáciles de mantener con componentes.

Astro funciona con la arquitectura MPA (Multi-Page Application) y la arquitectura de islas (Islands Architecture). Los frameworks MPA tradicionales usaban lenguajes de servidor clásicos, pero Astro trabaja con JavaScript, lo que permite renderizar componentes tanto en el servidor como en el cliente.

La arquitectura de islas significa que Astro maneja pequeñas aplicaciones o islas independientes para agregar interactividad, utilizando componentes de Astro o funcionando como un contenedor para agregar componentes de React, Vue o Solid.

La arquitectura de islas no es exclusiva de Astro. Otros frameworks también la implementan, aunque con diferentes enfoques:

  • Eleventy con Preact: combina Eleventy y Preact usando el wrapper WithHydration para hidratar los componentes en el cliente.

  • Marko: envía únicamente el código de hidratación necesario para los componentes interactivos y cambiar el estado del navegador, optimizando a través del compilador si el código corre en el cliente o en el servidor.

Ya tenemos un panorama claro sobre qué es Astro, pero la mejor forma de aprender es construyendo algo desde cero.

El proyecto

Vamos a construir una aplicación que consume la API de Rick and Morty. La app muestra una lista de personajes en la página principal y permite hacer clic en cualquiera de ellos para ver su página de detalle.

Para este proyecto necesitamos crear los siguientes elementos:

  • Componentes: Header, Navigation, Footer, Character List y Character.

  • Páginas: Home y CharacterDetail.

  • API: funciones para proveer los datos.

Crear el proyecto

Abre tu terminal y ejecuta el comando npm create astro@latest. Esto iniciará un asistente interactivo con varias preguntas. Elige la opción Just the basics y activa TypeScript en modo estricto para dejar todo listo para empezar.

npm create astro@latest
npx: installed 78 in 8.962s
+, , , , , +  Houston:
| ^ u ^  I'll be your assistant today.
+, , , , , +
 astro   v1.6.12 Launch sequence initiated.
 
? Where would you like to create your new project? » astro_rick_morty

La opción Just the basics crea una estructura mínima para nuestro proyecto. Esto nos ayuda a explicar algunos conceptos clave de Astro.

Estructura del proyecto

Ya tenemos el proyecto creado. Entremos en el directorio para revisar cada carpeta:

  • public: para almacenar archivos estáticos como imágenes.

  • pages: todos los archivos dentro de pages son componentes y funcionan automáticamente como rutas.

  • layouts: componentes que sirven como contenedores para reutilizar secciones en todas las páginas.

  • components: aquí guardamos todos los componentes reutilizables de la app.

  • astro.config: nos ayuda a definir otros frameworks para usar en la app (si los necesitamos).

Borra los archivos generados Card.astro, Layout.astro e index.astro para empezar desde cero.

Crea de nuevo el archivo index.astro, ejecuta el comando en la terminal y levanta el servidor local en el puerto 3000 con una página vacía en el navegador:

astro dev
   astro  v1.6.12 started in 36ms
  ┃ Local    http://127.0.0.1:3000/
  ┃ Network  use --host to expose

El archivo de Astro

Antes de crear los componentes, quiero explicarte las dos secciones principales de un componente de Astro: el ComponentScript y el ComponentTemplate.

ComponentScript:

Es el bloque de TypeScript delimitado por guiones ---, igual que el frontmatter en los archivos Markdown. Permite importar componentes de Astro o de otros frameworks, así como declarar variables y funciones para utilizarlas en el ComponentTemplate.

---
const hello = (name: string) => {
	return `Hello${name}!`
}
const title = 'Welcome Astro'
---

Component Template

Renderiza HTML o componentes importados y soporta expresiones de JavaScript y directivas de Astro.

La sintaxis de componentes de Astro es un superconjunto de HTML (algo similar a lo que TypeScript es para JavaScript). Nos permite utilizar expresiones parecidas a JSX, pero no es JSX.

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<h1>{title}</h1>
		{ hello('Bezael')}
	</body>
</html>

Ya tenemos una vista general de cómo funcionan los componentes en Astro. Ahora vamos a construir los nuestros.

Puedes leer más sobre componentes en Astro:

Agregar Tailwind

Queremos darle estilos a la app, y con Astro podemos integrar Tailwind CSS fácilmente con un solo comando que configura todo de forma automática:

npx astro add tailwind

Puedes consultar más detalles en la guía de integración con Tailwind en la documentación de Astro.

Componentes

Comenzaremos con dos componentes sencillos: footer y navigation. Como vimos antes, cada componente tiene dos áreas principales.

El footer: crea el archivo footer.astro en la carpeta components/. Agrega la sección ComponentScript con --- y define la variable message con el valor 'Build with Love and Astro'. En la plantilla, muestra la variable usando llaves {}:

El código final se ve así:

---
const message = 'Build with Love and Astro'
---
<footer>
    {message}
</footer>

La navegación: de forma parecida al footer, crea el archivo navigation.astro y declara un arreglo en la sección ComponentScript. Cada elemento del arreglo es un objeto con dos propiedades: path y title.

const options = [{ path: '/', title: 'Home'}, { path: 'about', title: 'About'}];

Usa la función map en el ComponentTemplate con una expresión similar a JSX. Agrega el elemento de enlace para renderizar las opciones del menú dentro del map: {value}.

Note: Estoy usando algunas clases de Tailwind solo para dar estilo.

---
const options = [{ path: '/', title: 'Home'}, { path: 'about', title: 'About'}];
---
<div
class="flex flex-col items-center justify-center space-y-3 md:flex-row md:space-y-0 md:space-x-8  md:justify-end"
> 
        {options.map((p: any) => 
         <div class="group">
            <a href={p.path}>{p.title}</a>
            <div
              class="mx-2 mt-2 duration-500 border-b-2 opacity-0 border-black group-hover:opacity-100"
            ></div>
        </div>
        )}
</div>

Layout

Los layouts son componentes para proveer una interfaz de usuario reutilizable. Funcionan como un contenedor para las páginas, usando un slot para dar espacio donde inyectar el contenido de cada página (funciona de forma similar a router-outlet en Angular).

En la carpeta layouts/, crea el archivo layout.astro. En el ComponentScript, importa los componentes Footer y Navigation.

Agregamos la navegación, el pie de página y un slot para que Astro inyecte las páginas.

El código final se ve así:

---
import Footer from "../components/footer.astro";
import Navigation from "../components/navigation.astro";
---
<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
        <Navigation></Navigation>
        <div class="flex flex-col items-center justify-center min-h-screen m-2 bg-cyan-50">
                       
            <slot>
            
            </slot>
            <Footer></Footer>
 
        </div>
 
</body>
</html>

Puedes leer más sobre slots y layouts en la documentación de Astro.

Páginas

Las páginas son componentes dentro de pages/ responsables de gestionar el enrutamiento, cargar datos y renderizar el contenido de cada vista.

El componente index.astro es la primera página. Como queremos reutilizar la estructura común, importamos el componente Layout.

Agrega un elemento h1 con Hello Astro dentro del cuerpo de Layout. Astro renderizará este contenido en el área del slot definida en el layout.

El código final se ve así:

---
import Layout from "../layouts/layout.astro";
---
<Layout>
<h1>
    Hello from Astro
    </h1>
</Layout>

Guarda los cambios y verás el mensaje Hello from Astro con los estilos definidos en el layout.

Obtener datos desde la API

Los componentes CharacterList y Character necesitan datos para enlazarlos y renderizarlos, así que primero vamos a crear las funciones para consultar la información.

Como vengo del ecosistema de Angular, me gusta organizar la lógica creando un directorio services/ con el archivo rickmorty.service.ts.

Este servicio tiene cinco funciones:

  • getAPICharacters: obtiene la lista de todos los personajes desde la API.

  • getAPISingleCharacter: obtiene un personaje individual a partir de su ID.

  • transformData: transforma la respuesta y retorna únicamente las propiedades que nos interesan.

  • getSingleCharacterDetail: retorna el personaje según su ID.

  • getCharacters: retorna todos los personajes listos para usar.

Como la API de Rick and Morty tiene una estructura definida, creamos las interfaces de TypeScript correspondientes para cada entidad:

 
export type APICharacterResponse = {
    results: APICharacterModel[]
}
 
export interface APICharacterModel {
    id:       number;
    name:     string;
    status:   string;
    species:  string;
    type:     string;
    gender:   string;
    origin:   Location;
    location: Location;
    image:    string;
    episode:  any[];
    url:      string;
    created:  Date;
}
 
export interface Location {
    name: string;
    url:  string;
}

Llamadas a la API

Sin entrar en detalles complejos, usamos async, await y fetch para consultar los datos:

const getAPICharacters = async (): Promise<APICharacterResponse> => {
    const charactersRequest = await fetch(API);
    return charactersRequest.json();
}
 
const getAPISingleCharacter = async (id: string) => {
    const request = await fetch(`${API}/${id}`);
    return request.json();
}

Como la respuesta de la API es un objeto grande con campos que no necesitamos, creamos la función transformData:

const transformData = (characters: APICharacterResponse) => {
    return characters.results.map((c) => {
 
        return {
            id: c.id,
            name: c.name,
            image: c.image
        }
    });
 
}

Por último, creamos dos funciones que consumiremos desde los componentes: getCharacters y getSingleCharacterDetail. Cada una llama al método correspondiente para consultar la información:

export const getCharacters = async () => {
    const characters = await getAPICharacters();
    return transformData(characters);
}
 
 
export const getSingleCharacterDetail = async (id: string) => {
 
    return await getAPISingleCharacter(id);
 
}

Puedes aprender más sobre async y await y fetch en MDN.

Astro.props y componentes

Ahora necesitamos crear dos componentes y pasarles datos:

  • character: muestra la imagen y el nombre del personaje.

  • characterlist: asume la responsabilidad de listar todos los personajes.

Para pasar datos a los componentes, usamos Astro.props para leer los valores pasados en los atributos del componente, por ejemplo:

<my-component title="hi" id=""/>

Crea el archivo character.astro. En la sección de script, extrae los atributos para el componente (name e image) desde Astro.props:

En la plantilla HTML, usamos las variables como hicimos antes:

El código final se ve así:

---
const { name, image } =  Astro.props;
---
<div class="relative group">
    <img  alt={name} width="100%" height="100%"  src={image} />
    <div
    class="absolute bottom-0 left-0 right-0 p-2 px-4 text-white duration-500 bg-black opacity-0 group-hover:opacity-100 bg-opacity-40"
  >
    <div class="flex justify-between w-full">
      <div class="font-normal">
        <p class="text-sm">{name}</p>
      </div>
    </div>
  </div>
</div>

A continuación, trabajamos con characterlist.astro. Este componente utiliza el componente Character, recibe una lista de personajes y recorre el arreglo de forma similar a como hicimos en Navigation.

Primero, importa el componente Character y extrae characters desde Astro.props:

---
import Character from "./character.astro";
const { characters } = Astro.props;
---

En la plantilla del componente, iteramos sobre el arreglo de personajes y renderizamos el componente:

El código final se ve así:

---
import Character from "./character.astro";
const { characters } = Astro.props;
---
 
<div class="grid gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
  {
    characters.map((character: any) => (
        <Character name={character.name} image={character.image}></Character>
    ))
  }
</div>

Mostrar los datos

Estamos en los pasos finales. En este punto queremos mostrar la lista de personajes obteniendo los datos desde la API.

Abre index.astro e importa el componente Characterlist y la función getCharacters desde rickmorty.service.

Define la variable characters para guardar los datos de la función getCharacters usando la palabra clave await. En la plantilla, pasa los personajes al componente Characterlist:

El código final se ve así:

---
import Characterlist from "../components/characterlist.astro";
import Layout from "../layouts/layout.astro";
import { getCharacters } from "../services/rickmorty.service";
 
const characters = await getCharacters();
---
<Layout>
<Characterlist characters={characters}></Characterlist>
</Layout>

Guarda los cambios y revisa la lista de personajes en el navegador.

¡Perfecto! Ya tenemos la lista, pero ahora queremos hacer clic en un personaje y ver sus detalles.

Pasar parámetros

Astro soporta parámetros estáticos y dinámicos, así como rutas dinámicas utilizando nombres de archivo como characters/[id].astro y leyéndolos mediante Astro.params.

Para los parámetros dinámicos en el servidor debemos configurar el modo 'server', ya que por defecto Astro usa modo estático. Para cambiarlo, abre astro.config.mjs y define la propiedad output como 'server' dentro de defineConfig:

export default defineConfig({
  output: 'server',
  integrations: [tailwind()]
});

A continuación, crea un nuevo archivo en la carpeta pages/, llamado characters/[id].astro.

Importa el componente Character y la función getSingleCharacterDetail.

Obtén el id desde Astro.params y pásalo a getSingleCharacterDetail. Usando await, recibe la respuesta y extrae name e image.

El código final de [id].astro se ve así:

---
 
import Character from "../../components/character.astro";
import Layout from "../../layouts/layout.astro";
import { getSingleCharacterDetail } from "../../services/rickmorty.service";
 
const { id = '' } = Astro.params;
const  {name, image}  =  await getSingleCharacterDetail(id);
---
<Layout>
<Character name={name} image={image}></Character>
</Layout>

Para permitir que el usuario haga clic en una tarjeta y navegue al detalle, edita el componente characterlist.astro y envuelve el componente con una etiqueta <a>, pasando en href la ruta dinámica characters/${character.id}:

El código final se ve así:

---
import Character from "./character.astro";
const { characters } = Astro.props;
---
 
<div class="grid gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
  {
    characters.map((character: any) => (
      <a href={`characters/${character.id}`}>
        <Character name={character.name} image={character.image}></Character>
      </a>
    ))
  }
</div>

Guarda los cambios y haz clic para navegar a los detalles de cada personaje.

Despliegue

Ya creamos una aplicación básica con Astro, pero aún nos falta el despliegue. Astro ofrece una forma muy simple de desplegar y una excelente documentación. En mi ejemplo estoy usando Vercel.

El equipo de Astro ofrece adaptadores para desplegar rápidamente y configurar astro.config.mjs con un solo comando:

npx astro add vercel

Puedes leer más en la guía sobre despliegue en Vercel.

Resumen

Aprender las bases de Astro nos permite ver lo sencillo que es construir una aplicación. Astro resulta una excelente opción para construir sitios web estáticos, pudiendo integrar frameworks como React, Vue o Svelte únicamente si necesitamos interactividad avanzada.

Te invito a revisar la web en producción o explorar el código en GitHub:

Parte de la serie Frontend

Modern frontend development — Angular, React, TypeScript, accessibility, and performance.

Ver serie completa

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.