Navigation
Recherche
|
Astro with HTMX: Server-side rendering made easy
mercredi 19 mars 2025, 10:00 , par InfoWorld
![]() My previous article introduced the basics of dynamic web application development with Astro. Now, we’ll go deeper with a look at the code and build process for the to-do demo application. Server-side rendering with Astro and HTMX Although Astro is best known as a server-side rendering (SSR) meta-framework that supports reactive front ends like React and Vue as plugins, it has evolved into an impressive back-end solution in its own right, with endpoints and routing to handle almost anything you throw at it. In this demo, we’re going to put Astro through its server-side paces by using it to host HTMX views. What’s challenging (and interesting) about this approach is that we’ll be sending view fragments in our responses. That will require some finagling, as you’ll see. To start, we can launch a new application with the standard Astro command-line tool (see the Astro.js documentation if you need information about installing and using the Astro CLI): $ npm create astro@latest We’ll be using dynamic endpoints, so Astro will need to know what kind of deployment adapter to use (see my previous article for a discussion of adapters). In our case, we’ll use the adapter for a Node integration: $ npx astro add node Services Let’s start building our custom code at the service layer. The service layer gives us a central place to put all our middleware that can be reused across the app. In a real application, the service layer would interact with a data store via a data layer, but for our exploration we can just us in-memory data. The convention in Astro seems to be to use a /lib directory for these types of things. All the code for Astro goes in the /src directory, so we’ll have our service code at src/lib/todo.js: src/lib/todo.js: // src/lib/todo.js let todosData = [ { id: 1, text: 'Learn Kung Fu', completed: false }, { id: 2, text: 'Watch Westminster', completed: true }, { id: 3, text: 'Study Vedanta', completed: false }, ]; export async function getTodos() { return new Promise((resolve) => { setTimeout(() => { resolve(todosData); }, 100); // Simulate a slight delay for fetching }); } export async function deleteTodo(id) { return new Promise((resolve) => { setTimeout(() => { todosData = todosData.filter(todo => todo.id!== id); resolve(true); }, 100); }); } export async function addTodo(text) { return new Promise((resolve) => { setTimeout(() => { const newTodo = { id: todosData.length+1, text, completed: false }; todosData.push(newTodo); resolve(newTodo); }, 100); }); } One thing to notice in general is that all our functions return promises. This is something Astro supports out of the box and is excellent for these service methods when they need to talk to a datastore, to avoid blocking on those network calls. In our case, we simulate a network delay with a timeout. Otherwise, these are vanilla JavaScript calls that use some simple functional operations to perform the work we need on the in-memory array of todosData. That’s all we need for the service layer. The main view Now let’s consider src/pages/index.astro. The astro create command put a simple welcome page in there, which we can repurpose. To start, we delete all the references to the Welcome component. Instead, we’ll use our own TodoList component, which we’ll build in a moment: ---- // src/pages/index.astro import Layout from '../layouts/Layout.astro'; import TodoList from '../components/TodoList.astro'; --- In Astro components (defined inside.astro files) we have two segments: the JavaScript inside the “code braces” (----) and the HTML-based template. This is similar to other templating technologies. But Astro is somewhat unique because by default everything is run on the server and packaged into an HTML bundle with minimal JavaScript, which is then sent to the client. Reusable components Now let’s take a look at the TodoList component. The components directory holds all our reusable.astro components, so TodoList is found in src/components/TodoList.astro: --- // src/components/TodoList.astro import { getTodos, deleteTodo } from '../lib/todo'; import TodoItem from './TodoItem.astro'; const todos = await getTodos(); --- Add Todo {todos.map(todo => ( ))} As a side note, I’ve included a snippet of styles. Astro makes it easy to include component-scoped CSS as we’ve done here. I won’t discuss CSS much here because we are focused on the structure and logic of the Astro application. TodoList imports the necessary functions from the service module we just saw, and uses them to render a view. First it uses await to grab the to-do’s, then in the template it loops over them with todos.map. For each Todo, we use the TodoItem component and pass in a property holding the to-do data. We’ll take a look at the TodoItem item shortly. There’s also a form that is used to create new to-do’s. This uses HTMX to submit the form with background AJAX and avoid a page reload: hx-post: Tells it to submit a POST request to /api/todos. hx-target: Indicates where to put the response (into the to-do list element). hx-swap: Fine-tunes how to add the new element (at the end of the list). This whole part of the UI will be rendered ahead of time on the server and sent pre-packaged to the browser. TodoItem component Before looking at the API that will field the create requests, let’s look at the TodoItem component, at src/components/TodoItem.astro: --- // src/components/TodoItem.astro import { deleteTodo, getTodos } from '../services/todo'; export interface Props { todo: { id: number; text: string; completed: boolean }; } const { todo } = Astro.props; --- {todo.text} {todo.completed? ' (Completed)': ''} Delete The TodoItem accepts the properties we saw earlier. (See the Astro documentation to learn more about Astro properties, or props, including TypeScript interfaces.) Using the props we create a simple list item for the todo and a Delete button that also uses some HTMX to handle deletion via AJAX. The HTMX here uses hx-delete, which submits a DELETE request. The hx-target and hx-swap attributes show off some of the power of HTMX in these simple properties, allowing us to target the list item itself for deletion. (The hx-delete by default removes its target element upon receiving an HTTP success response.) Delete endpoint Next, let’s look at how we can field the DELETE request. Astro’s file-based routing lets us define our server endpoints inside /pages, alongside the views, using the same routing semantics. We’ll create an /api subdirectory to help organize things, and we are using a route parameter to capture the todo ID that was submitted for deletion, giving us the following file: src/pages/api/todos/[id].js: import { deleteTodo, getTodos } from '../../../lib/todo'; export const prerender = false; export async function DELETE({ params, request }) { const id = parseInt(params.id, 10); if (isNaN(id)) { return new Response(null, { status: 400, statusText: 'Invalid ID' }); } await deleteTodo(id); return new Response(null, { status: 200 }); // Empty response is sufficient for delete } Astro endpoints are just like views except without the template part. One important general note is that we use the prerender = false flag to ensure the engine doesn’t try to create this endpoint when building. We want a dynamic endpoint. (The Astro docs refer to getStaticPaths() as a way to fine-tune which endpoint functions are dynamic. In our case, prerender lets us indicate everything is dynamic.) This endpoint is denoted as a DELETE and uses the service function to do the work on the data. It sends an empty Response object back with a 200 success code. When the HTMX gets that on the front end, it’ll remove the item from the view. Todo creation endpoint The last major piece of the puzzle is handling the todo creation endpoint. We need to accept the text of the new todo, add it to the list, and then send back the markup to be inserted into the list. This would in most cases be done with another server endpoint. However, Astro is still working on the ability to render components programmatically. (You can follow the work in the Container API render components in isolation roadmap and proposal.) For now, we can use a fairly painless workaround, and use a page view to handle the request and reuse the TodoItem.astro component to send a response fragment. This keeps our code DRY. Here’s what our pseudo-endpoint looks like at src/pages/api/todos/index.astro: --- import {addTodo} from '../../../lib/todo.js'; import TodoItem from '../../../components/TodoItem.astro'; let newTodo = null; if (Astro.request.method === 'POST') { const formData = await Astro.request.formData(); newTodo = await addTodo(formData.get('text')); } export const prerender = false; --- You’ll notice the JavaScript in the component front matter has full access to the request, so we have no problem filtering according to the method type. In general, the JavaScript is a full-blown server-side function. Also, notice that we again indicate a dynamic component with prerender = false. The main work is in turning the request form body into a new to-do item using the create function from our service utility. Then we use that as the prop on the TodoItem component. The net result is the response will be what is rendered by TodoItem.astro, using the new item data. Run it To run the app in dev mode, enter: $ npx astro dev To create a production build (output to /dist), enter: $ npx astro build Impressions of the Astro.js development experience Astro is nice to work with. Its dev mode is fast and pretty stable, and it is very good at only reloading chunks that have been modified. It also presents helpful errors in the browser, with hotlinks to relevant docs like this one: Matthew Tyson This type of error reporting demonstrates a high degree of care about developer experience. We did hit a kind of edge case with that todo creation endpoint but the workaround wasn’t too painful. Moreover, the Astro.js project is hot on the trail of creating an official solution. It’s tough to compete in this field with so many excellent and established frameworks, but it’s safe to say Astro is doing it. See my GitHub repository for all the demo code from this article.
https://www.infoworld.com/article/3847131/astro-with-htmx-server-side-rendering-made-easy.html
Voir aussi |
56 sources (32 en français)
Date Actuelle
jeu. 20 mars - 05:59 CET
|