Integrating Spin with Static Site Generators
David Flanagan
frontend
astro
static-site-generators
Building your backend with Spin is a fantastic way to provide your developers with a strong developer experience that doesn’t need virtual machines or containers to provide consistency, but how do we integrate this with our frontend?
Serverless backends are great, but we don’t expect our users to bust out curl
to use them, so let’s see how to integrate a static website with our Spin backend.
Static websites are a fantastic approach to providing a frontend to serverless functions, because they’re fast, cheap, and easy to deploy.
They’re fast because they’re just HTML, CSS, and JavaScript; typically rendered in advance and served from a simple HTTP server; meaning once they’ve delivered to your clients, they’re ready for the DOM to be visualized: no database or client side requests required.
Developers have been adopting static site generators, such as Gatsby, Hugo, and Jekyll, for years now because they provide the best Lighthouse score you can get.
In today’s article, we’ll take a look at using such a framework to provide an interface to your Spin functions.
Our Spin Backend
Our Spin backend provides a function that allows us to interface with OpenAI’s ChatGPT. Spin as a backend is GREAT for working with external APIs, because making outbound HTTP requests is a piece of cake with the fetch
API (JS/TS), or the SDK helpers for Rust and others.
Here’s our code for the backend:
const encoder = new TextEncoder("utf-8");
export async function handleRequest(request) {
const apiKey = spinSdk.config.get("openai_token");
const queryString = await request.text();
const formData = new URLSearchParams(queryString);
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: `What language is this? '${formData.get("text")}'`,
},
],
temperature: 0.7,
}),
});
const data = await response.json();
return {
status: 200,
body: encoder.encode(data.choices[0].message.content).buffer,
};
}
Of course, we can consume this API with hurl or curl, but we want to provide a frontend for our users.
Building Our Static Website
For today’s example, we’re going to use Astro to build our static website. Astro is pretty new, but also pretty awesome. It’s a framework for building static websites that doesn’t restrict you to a single UI framework, such as React. So you can use React, Svelte, Vue, WebComponents, or any combination of them.
To create a new Astro website, you can run the following command:
npm create astro@latest
We’re not going to focus on the Astro specifics today, other than the components we require and the config for static site generation; so if you wish to learn more you can do so on their getting started guide.
Configuring for SSG
Astro, by default, is already configured this way. However, some templates do come with a slightly different configuration. So, to ensure we’re all on the same page, let’s take a look at our astro.config.mjs
file:
export default defineConfig({
output: "static",
});
The output
should be "static"
, OR the output
property should be omitted from defineConfig
. If you see output
set to any other value, it’s likely your template has been configured for server-side rendering, rather than static site generation.
Our Frontend
Astro uses it’s pages
directory to store the pages of our website. So, let’s edit the index.astro
file in the pages
directory.
We’re just going to augment the default homepage with our integration to get the demo working. As such we need to add a form to submit requests to our backend endpoint.
<form action="/api" method="post">
<input type="text" name="text" placeholder="Enter Language Text" />
<input type="submit" value="Determine Language" />
</form>
We can use relative paths for the form submission because we’re deploying EVERYTHING together, either with spin up
locally or with spin deploy
to Fermyon Cloud.
Our form is as basic as it gets, we’re just asking the user to provide some text and we’ll use our backend to determine the language.
While I’m using a simple form, your frontend team can actually use their knowledge and experience to build this on React, Svelte, and so forth. They don’t need to throw away their knowledge to integrate Spin.
Here’s the full version:
---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
---
<html lang="en">
<head>
<BaseHead title="{SITE_TITLE}" description="{SITE_DESCRIPTION}" />
</head>
<body>
<header title="{SITE_TITLE}" />
<main>
<h1>🧑🚀 Hello, Spin Fans!!</h1>
<form action="/api" method="post">
<input type="text" name="text" placeholder="Enter Language Text" />
<input type="submit" value="Determine Language" />
</form>
<p>
Welcome to the official <a href="https://astro.build/">Astro</a> blog
starter template. This template serves as a lightweight,
minimally-styled starting point for anyone looking to build a personal
website, blog, or portfolio with Astro.
</p>
<p class="rawkode">
This template comes with a few integrations already configured in your
<code>astro.config.mjs</code> file. You can customize your setup with
<a href="https://astro.build/integrations">Astro Integrations</a> to add
tools like Tailwind, React, or Vue to your project.
</p>
<p>Here are a few ideas on how to get started with the template:</p>
<ul>
<li>Edit this page in <code>src/pages/index.astro</code></li>
<li>
Edit the site header items in <code>src/components/Header.astro</code>
</li>
<li>
Add your name to the footer in
<code>src/components/Footer.astro</code>
</li>
<li>
Check out the included blog posts in <code>src/pages/blog/</code>
</li>
<li>
Customize the blog post page layout in
<code>src/layouts/BlogPost.astro</code>
</li>
</ul>
<p>
Have fun! If you get stuck, remember to
<a href="https://docs.astro.build/">read the docs </a> or
<a href="https://astro.build/chat">join us on Discord</a> to ask
questions.
</p>
<p>
Looking for a blog template with a bit more personality? Check out
<a href="https://github.com/Charca/astro-blog-template"
>astro-blog-template
</a>
by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
</p>
</main>
<footer />
</body>
</html>
Testing Our Integration
Now that Astro has it’s index page configured with our form, we can spin everything up and test it out.
In-order to use Spin, we want a directory structure that allows our spin.toml
to be configured to point to each of our projects.
For today’s demo, we have the following directory structure:
.
├── frontend
│ ├── dist
│ ├── public
│ └── src
├── src
└── target
We have our frontend in the frontend
directory, and our backend in the src
directory. This allows us to configure our spin.toml
file as follows:
spin_manifest_version = "1"
authors = ["David Flanagan <david@rawkode.dev>"]
description = ""
name = "real-app-openai-language-guesser"
trigger = { type = "http", base = "/" }
version = "0.1.0"
[[component]]
id = "real-app-openai-language-guesser"
source = "target/spin-http-js.wasm"
exclude_files = ["**/node_modules"]
allowed_http_hosts = ["https://api.openai.com"]
[component.config]
openai_token = "{{ openai_token }}"
[component.trigger]
route = "/api"
[component.build]
command = "npm run build"
[variables]
openai_token = { required = true }
[[component]]
source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.2/spin_static_fs.wasm", digest = "sha256:65456bf4e84cf81b62075e761b2b0afaffaef2d0aeda521b245150f76b96421b" }
id = "frontend"
files = [{ source = "frontend/dist", destination = "/" }]
environment = { FALLBACK_PATH = "index.html" }
[component.trigger]
route = "/..."
[component.build]
command = "npm run build"
Things that are important here. First, our backend is bound to the /api
path:
[component.trigger]
route = "/api"
We’re also allowing it to speak to a remote host, for sending requests to OpenAI:
allowed_http_hosts = ["https://api.openai.com"]
Finally, our backend is configured to use the openai_token
variable:
[component.config]
openai_token = "{{ openai_token }}"
which we provide with an environment variable:
export SPIN_CONFIG_OPENAI_TOKEN="..."
Next, we can use the Spin file-server to host our static website:
[[component]]
source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.0.2/spin_static_fs.wasm", digest = "sha256:65456bf4e84cf81b62075e761b2b0afaffaef2d0aeda521b245150f76b96421b" }
id = "frontend"
files = [{ source = "frontend/dist", destination = "/" }]
By default, the Spin file-server uses index.html
as a fallback for missing files and as a directory index: so it’ll do the right thing for most frontend frameworks.
We’re also specifically pointing to the frontend/dist
directory for the static assets, which is also used by most frontend frameworks for the output.
For this to work well, we configure the file-server component to match any paths not /api
with a wildcard:
[component.trigger]
route = "/..."
Finally, we don’t want to force our developers to run the build command for their website manually, so we hook this up to Spin:
[component.build]
command = "npm run build"
Now, we can run spin build
and everything will be built, ready to be run:
spin build
spin up
Of course, if you could also run this with a single command:
spin build --up
Watching for Changes
This doesn’t need to end here! As developers iterate on their changes, we want them to get real-time updates in their browser. To do so, we can hook into spin watch
to build and relaunch their changes as soon as they happen.
To do so, we need to configure which files to watch for the frontend component:
[component.build]
watch = ["src/**/*", "public/**/*"]
Simple, right? Now just run spin watch
.
Seeing it in Action
Want to see it working? Check it out.
Conclusion
Spin is a fantastic way to provide a backend for your frontend developers. It’s easy to integrate with your existing tooling, and it’s easy to integrate with your existing workflows.
You don’t need to throw away your knowledge and experience with frontend frameworks to hook into the power and performance of the Spin and WebAssembly for your backend.
To learn more about Spin, check out the documentation, or join the community on Discord.
Until next time 🤘🏻