How to Build Your First App with Astro

How to Build Your First App with Astro

Starting Your Journey with the Astro Framework

For a few years, web projects have been taking more and more responsibility, which means more and more JavaScript.

Nowadays, the top SPA Frameworks, React, Vue, or Angular, have great developer experience and allow us to build complex applications but have some pain points.

  • The size of JavaScript to Browsers.

  • The SEO doesn't like the SPA.

Each framework tries to solve the problem with bundle strategies, split in modules, lazy loading, and SSR to try to patch the problem. The hydration process is so heavy for the browser, so our issues still need to be solved 100%.

What can we do?

The web is continuously evolving, and some solutions exist to help us with performance and SEO, and today I will talk about Astro.

What is Astro?

Astro is a “static” website generator focusing on performance and SEO, providing interactivity with Astro components or using the power from React Vue or Svelte.

Astro helps us move away from heavy JavaScript Websites but keep the power of use components scalable and maintainable websites.

Astro works with MPA and Island architecture, the old MPA frameworks use server language, but Astro works with Javascript, which allows rendering components on the server and client side.

The Island Architecture means Astro works with small apps or islands to add interactivity, using Astro components or working as a container to add React, Vue, or Solid components.

The Island architecture is not unique to Astro. Other frameworks are, but with other approaches:

  • Eleventy with Preact: Using the combination of Eleventy and Preact with theWithHydration wrapper to hydrate the components on the client.

  • Marko: it ships the interactive component's hydration code to change the browser's state, so the Marko compiler optimizes where it runs, client or server.

We already have an overview of Astro, but the best way to learn is by building something from zero.

The Project

We will build an app and consume the Rick And Morty API. The app shows a list of characters on the home page, click on details and go to the details page when clicking on a single character.

We must create an Astro project with the following:

  • Components: Header, Navigation, Footer, Character List, Character

  • Pages: Home and CharacterDetail.

  • API: Functions to provide the data.

Create The Project

Using the terminal, run the following command npm create astro@latest. It will trigger an assistant to ask questions and pick Just the basics . Use Typescript strictly to set everything ready to start.

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

The option Just the basic creates an essential structure for us. It helps us to explain some points about Astro.

Project Structure

We have the project; let's navigate into the project to see every directory:

  • public: to store static files like images.

  • pages: All files in the pages are components and work as routes.

  • layouts: The components work as containers to reuse sections on all pages.

  • components: store all components to use in the app.

  • astro.config: help us to define other frameworks to use in the app (if we need them)

Delete the generated files Card.astro , Layout.astro and index.astro, to start from zero.

Create the index.astro file again, run the terminal command, and start the local server in port 3000 with an empty page in the browser.

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

The Astro File

Before creating the components, I want to give an overview of the Astro Components sections, the ComponentScript and ComponentTemplate.

ComponentScript:

It is the Typescript area defined with --- a front matter like markdown files and allows import of Astro or framework components declares variables, and functions to use in the ComponentTemplate.

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

Component Template

It renders HTML or imported components and supports Javascript expression and Astro directives.

The Astro Component syntax is a superset of HTML (Like Typescript for Javascript), allowing us JSX-like expression but is not 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>

We have already finished an overview of Astro components. Let's build our components.

Read more about components in Astro.

Add Tailwind

We want some app styling to easily integrate Astro with a single command and configure everything automatically.

npx astro add tailwind

Read Tailwind integration in Astro docs.

Components

We will start with two simple components, the footer and navigation, we learn each component has two main areas.

The footer: create a footer.astro file in the components directory, and add ComponentScript area with --- and create the variable message with the Build with Love and Astro. In the template, add a message variable using the {}:

The final code looks like this:

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

The navigation: Similar to the footer, create navigation.astro and declare an array n the ComponentScript area. The object for navigation is an array, and the object has two values path and title.

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

Use the map function in the ComponentTemplate using a JSX-like expression. Add the link element to render the menu properties inside the map. {value}.

Note: I'm using some tailwind classes just styling.

---
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

The Layouts are components to provide reusable UI; they work as a container for the pages combined with the to give space to inject the pages. (It works like the router-outlet in Angular).

In the layouts, the directory creates the layout.astro file; in the ComponentScript, import Footer and Navigation components.

We added the Navigation and Footer and a slot to Astro inject the pages.

The final code looks like this:

---
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>

Read more about slot and Layout

Pages

The pages are components in the pages/ responsible for managing the routing, load data, and page content.

The index.astro component is the first page; we want to reuse the layout component and import the Layout component.

Add h1 with Hello Astro in the Layout body, Astro renders the content in the slot area defined in the layout.

The final code looks like this:

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

Save changes, the Hello from Astro the message is shown with the layout style.

Get Data From API

The CharacterList and Character need data to bind to the components to interact, so we are going first to create the functions to get the data

Because I'm an Angular guy, I created the directory services with the file rickmorty.service.ts.

The service has five functions:

  • getAPICharacters: Get the list of all characters from API.

  • getAPISingleCharacter: Get a single character from ID.

  • transformData: transform the response and return some properties.

  • getSingleCharacterDetail: return the Character from ID.

  • getCharacters: return all Characters.

Because the rickandmorty API has a structure; it creates an interface for each entity.


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;
}

API Calls

I won't go into deep details, but We use the async await and fetch to get the data.

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();
}

Because the API Response is a complex object, we create the transformData method.

const transformData = (characters: APICharacterResponse) => {
    return characters.results.map((c) => {

        return {
            id: c.id,
            name: c.name,
            image: c.image
        }
    });

}

Finally, create two functions from the components getCharacters and getSingleCharacterDetail. Each one calls a specific method to get the data from API.

export const getCharacters = async () => {
    const characters = await getAPICharacters();
    return transformData(characters);
}


export const getSingleCharacterDetail = async (id: string) => {

    return await getAPISingleCharacter(id);

}

Learn more about async, await, and fetch

Astro.props And Components

We need to create two components and pass data to it:

  • character: show a character image and name.

  • characterlist: take the responsibility to list all characters.

To pass the data to the components, we use Astro.props to read values from component attributes, attributes like:

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

Create the character.astro file in the script section, extract to attributes for the component: title and image from Astro.props

In the Markup, use the variables as we did before:

The final code looks like this:

---
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>

Next, we have to work with the CharacterList , which uses the Character components, gets a list of characters from the API, and iterates over the array similar to the Navigation.

First, import the Character component, and from Astro.props get the character

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

In the Component template, iterate over the character array and render the component.

The final code looks like this:

---
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>

Display the Data

We are in the final steps; in this step, we want to show the list of Characters and get the data from API.

Open the index.astro and import the CharacterList component and the getCharacter function from rickymorty.service

Define the characters variable to save the data from the getCharacter function using the await keyword. In the template, pass the characters to the CharacterList component.

The final code looks like this:

---
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>

Save and see the list of the Characters in the browser.

Perfect! We have the list, but I want to click on one character and see the details.

Passing Parameters

Astro supports static and dynamic parameters and dynamic routes using the file name like characters/[].astro and reading it using Astro.params

For the dynamic parameters, we must use the server() mode, by default, uses static. To change it, open the astro.config.mjs add the output in the defineConfig section to the server:

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

Next, create a new file in the pages section, like characters/[id].astro

Import the Character component and the getSingleCharacterDetail function.

Get the id from Astro.params and pass it to the getSingleCharacterDetail, and using await, get the response and extract the name and image.

The [id].astro final code looks like this:

---

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>

To allow the user to click in detail and navigate. Edit the characterlist.astro component and wrap the component with an <a in the href pass the route characters/${character.id}

The final code looks like this:

---
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>

Save and navigate to the character details!

Deploy

We already created a basic app with Astro, but we still need the deployment, and Astro has an easy way to deploy and excellent documentation. My example is using vercel

The Astro team has Adapters to deploy quickly and configure the astro.config.mjs with a single command:

npx astro add vercel

Read more about deploy

Recap

By learning a bit about Astro, we can see how simple it is to build an app. Astro appears to be a great option for constructing static websites with frameworks like React, Vue, or Svelte if we need more responsiveness.

Feel free to see the web or play with the GitHub code.