March 11, 2025

Protect Your REST APIs with Service Chaining

Matt Butcher Matt Butcher

spin fermyon python javascript

Protect Your REST APIs with Service Chaining

Spin applications are composed of one or more serverless functions. One serverless function can easily call to another function regardless of source language. In fact, it is easy to build REST API-style apps that function like microservices, but deploy in unison. And protecting REST APIs is simple.

In this post, we’ll write an app with two components — a REST API in Javascript and a simple frontend in Python. Then we’ll show how the Python serverless function can make a REST-style call to the Javascript component. But then we’ll take things one level further: We’ll preserve the feel of a REST-based microservice, but eliminate the network connection between the two. Finally, we’ll make the Javascript REST API private (internal-use only) to prevent external users or apps from accessing the endpoint while allowing our Python code to continue using it.

In this example, we’ll create a simple “book of the day” app with a REST-like API service that chooses a book and a Python front-end that renders a user-facing page.

Creating a Multi-component Project

A Spin app is composed of one or more components. Components act like serverless functions, receiving an event (like an HTTP request), doing some processing, and then returning a response. We’re going to build two such serverless functions.

Usually when we start a Spin project, we create a “top-level” component. Any additional components we create are nested inside that component. For example, to create a new Rust-based HTTP handler, we might run a command like spin new -t http-rust my-rust-app. And then we might later add another function with spin add -t http-js .....

It is also possible to create an empty project with no top-level components. In this case, while a spin.toml file will be created, no language-specific directories will be dropped into the app directory. This is useful when you are creating a multi-component Spin application as we are about to do.

To get going, we will start an empty project:

$ spin new -t http-empty bookserver
Description: A multi-component book service

Looking inside of this new project, we’ll see only a few basic files:

$ tree -a bookserver
bookserver
├── .gitignore
└── spin.toml

At this point, we can use spin add to add components one at a time. Next up, let’s create our Javascript API service.

Creating a Simple JSON Service in Javascript

The book-api service will return a book recommendation for the book of the day. We’re going to write it in Javascript. And it will be one of two components inside of our bookserver app.

$ cd bookserver
$ spin add -t http-js book-api     
Description: API server for recommending a book
HTTP path: /api
Enable AoT Compilation [y/N]: n

Now our project is a little bit larger:

$ tree -a bookserver
bookserver
├── .gitignore
├── book-api
│   ├── .gitignore
│   ├── knitwit.json
│   ├── package.json
│   ├── src
│   │   ├── index.js
│   │   └── spin.js
│   └── webpack.config.js
└── spin.toml

And now the top-level spin.toml points to our new JS service:

spin_manifest_version = 2

[application]
name = "bookserver"
version = "0.1.0"
authors = ["Matt Butcher <matt.butcher@fermyon.com>"]
description = "A multi-component book service"

# This is the new service we just added with `spin add...`
[[trigger.http]]
route = "/api"
component = "book-api"

[component.book-api]
source = "book-api/target/book-api.wasm"
allowed_outbound_hosts = []

[component.book-api.build]
command = "npm run build"
workdir = "book-api"

Before we go too far into coding, we need to make sure that we run npm install inside of the bookserver/book-api directory: cd book-api && npm install . That installs all the Javascript libraries we need.

Now we can start building our serverless function.

Our REST API will be very basic. It’ll recommend a different book for each day of the week. We’ll statically code all seven books in one array. Then, per request we will return a simple JSON object that looks like this: { "title": "BOOK TITLE", "author": "BOOK AUTHOR"}.

import { ResponseBuilder } from "@fermyon/spin-sdk";

let books = [
    {
        title: "Dead Souls",
        author: "Nokolai Gogol"
    },
    {
        title: "The Little Drummer Girl",
        author: "John Le Carré"
    },
    {
        title: "We Were Eight Years In Power",
        author: "Ta-Nehisi Coates"
    },
    {
        title: "My Ántonia",
        author: "Willa Cather"
    },
    {
        title: "Haroun and the Sea of Stories",
        author: "Salman Rushdie"
    },
    {
        title: "The Bird King",
        author: "G. Willow Wilson"
    },
    {
        title: "The Hobbit",
        author: "J.R.R. Tolkien"
    }
]

export async function handler(req, res) {
    let todaysBook = (new Date()).getDay();
    res.send(JSON.stringify(books[todaysBook]));
}

The handler function figures out what day of the week it is, looks up the relevant entry in the books array, and returns that object encoded as JSON.

That’s all there is to it. With a simple spin build --up in the bookserver directory, we can compile and run our app. Then, in another console we can test it out with curl:

$ curl localhost:3000/api 
{"title":"The Little Drummer Girl","author":"John Le Carré"}

I’m writing this post on a Monday, which is day 1 in Javascript’s 0-indexed day-of-week. So we are getting the correct result.

We’ve created a simple REST API in Javascript. Next, let’s write a simple frontend using Python.

Accessing the Javascript Service from Python

Just like last time, we’ll add a new component with spin add:

$ spin add -t http-py frontend 
Description: Book of the Day Frontend
HTTP path: /

Now, bookserver/frontend contains the template Python code.

Setting up a modern Python environment is a bit trickier than configuring Javascript, and will depend on how you configured your local toolchain. Read the Spin Python docs to see how we set up our Javascript environment using virtual environments. For my local environment, I followed the recommendations in the quickstart guide.

Regardless of how you configured your environment, at some point in your setup, you should do a pip3 install -r requirements.txt on the requirements file in bookserver/frontend.

Now that the toolchain is properly configured, we can edit the code in bookserver/frontend/app.py.

import json
from spin_sdk.http import IncomingHandler, Request, Response, send

class IncomingHandler(IncomingHandler):
    def handle_request(self, request: Request) -> Response:
        # Get the book of the day from JS:
        resp = send(Request("GET", "http://localhost:3000/api", {}, None))
        book = json.loads(resp.body.decode("utf-8"))
        msg = f"Today's book is _{book['title']}_ by {book['author']}"

        return Response(200, {"content-type": "text/plain"}, bytes(msg, "utf-8"))

The handle_request method is doing the following:

  1. Sending a request to our JS REST API on localhost:3000/api
  2. Reading the JSON response and parsing it into a dict stored in the variable book
  3. Creating a string msg that contains a the text of our response, and then…
  4. Returning an HTTP response with msg as the body

Before testing, we need to make one important change in the spin.toml file. We need to let Spin know that it is okay for our Python frontend to access the Javascript backend. By default, Spin apps are not allowed to make arbitrary outbound network connections. This is part of the security sandbox. But a small amount of configuration let’s us OK the REST API.

Here’s what our allowed_outbound_hosts looks like in the spin.toml configuration for our frontend component:

[component.frontend]
source = "frontend/app.wasm"
# Added this line to allow network connection to JS component
allowed_outbound_hosts = ["http://localhost:3000"]
[component.frontend.build]
command = "componentize-py -w spin-http componentize app -o app.wasm"
workdir = "frontend"
watch = ["*.py", "requirements.txt"]

With both the code and configuration changed, we can use spin build --up to compile and test our entire app. And then we can use a web browser or client like curl to see the result:

$ curl localhost:3000/
Today's book is _The Little Drummer Girl_ by John Le Carré

At this point, what we have is a REST API listening at localhost:3000/api and a frontend listening at localhost:3000/. And what is happening behind the scenes is that our Python code is making a network request to our Javascript service.

Our code above is making a network connection, using the localhost loopback interface, to access the REST API we wrote in the previous section. Phrasing this the long way, when a request comes in to the Python handler, Spin executes the Python code. Then when the Python send function is executed, Spin creates a new network connection from the Python code to the Javascript code and sends the request over that network connection. And that causes Spin to then execute the Javascript code. This is the way normal REST APIs work, and there is certainly nothing wrong with it.

But with a few minor changes, we can get rid of the network request and all of the overhead that entails and call directly from the Python code to the Javascript code in a way that still feels like a REST-style request.

Dropping Network Overhead with Local Service Chaining

In Spin apps, local service chaining allows you to call from one component to another without traversing the network (even on a loopback). Furthermore, it provides a way for us to optionally say, “This Javascript service can only be accessed by other parts of the app, and is not publicly available.”

FIrst, let’s do one quick pass over the Python code and configuration to avoid hard-coding the URL to the Javascript component:

import json
from spin_sdk.http import IncomingHandler, Request, Response, send

# Allows us to load the service URL from config, instead of hard-coding
from spin_sdk import variables

class IncomingHandler(IncomingHandler):
    def handle_request(self, request: Request) -> Response:
        # Get the URL from config
        url = variables.get("book_api_url")
        # Get the book of the day from JS:
        resp = send(Request("GET", url, {}, None))

        book = json.loads(resp.body.decode("utf-8"))
        msg = f"Today's book is _{book['title']}_ by {book['author']}"

        return Response(200, {"content-type": "text/plain"}, bytes(msg, "utf-8"))

In the code above, we replaced the hard-coded URL with one fetched from an application variable. We imported the variables object from spin_sdk and then used it to get an application variables called "book_api_url".

Now we just need to update the spin.toml with that variable. Here’s the relevant section of the spin.toml file:

[component.frontend]
allowed_outbound_hosts = ["http://localhost:3000"]
source = "frontend/app.wasm"

# Declare application-specific variables
[component.frontend.variables]
book_api_url = "http://localhost:3000/api"

[component.frontend.build]
command = "componentize-py -w spin-http componentize app -o app.wasm"
workdir = "frontend"
watch = ["*.py", "requirements.txt"]

As before, we’re pointing to http://localhost:3000/api as the endpoint for the Javascript REST API. If we rebuild and restart the app (using spin build --up) then we will see exactly the same behavior as last time.

But now let’s switch to using local service chaining and, in so doing, get rid of the needless overhead of opening a network connection between the Python component and the Javascript component.

# Declare application-specific variables
[component.frontend.variables]
book_api_url = "http://book-api.spin.internal/api"

Now we are pointing to a special spin.internal URL. When Spin sees a request to something inside of spin.internal, it will look for a component that has a matching name, and invoke that directly instead of creating a socket connection. Recalling our earlier configuration, we named the Javascript service book-api. So the above URL informs Spin to direct the request directly to the Javascript service. Spin executes that component in place (again, without routing it down to the networking layer and opening a socket). From the developer perspective, it looks and feels exactly like working with any REST service, but it is much faster to execute and avoids a needless HTTP transaction. In local testing, I was able to get a 10% performance boost when switching to service chaining.

Making the REST service private

We can add one more bit of configuration and improve our security. If we don’t want the Javascript service to be accessible by anything other than our app, we can annotate its configuration to tell Spin to only expose it using local service chain.

[[trigger.http]]
# Old config: Public route
# route = "/api"
# New config: Make the route private
route = { private = true }
component = "book-api"

Before, if I used curl to access http://localhost:3000/api, I would get something like this:

$ curl localhost:3000/api 
{"title":"The Little Drummer Girl","author":"John Le Carré"}

But now, because we have made that route private, we will instead get a 404 Not Found error:

$ curl -v localhost:3000/api
* Host localhost:3000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:3000...
* connect to ::1 port 3000 from ::1 port 57309 failed: Connection refused
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000
> GET /api HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 404 Not Found
< content-length: 0
< date: Tue, 28 Jan 2025 18:40:52 GMT
< 
* Connection #0 to host localhost left intact

Note that we don’t even have to recompile the code for this change to take effect. It’s merely a matter of restarting the service with the updated config.

When Is This Useful?

In many ways, using local service chaining is great simply for the security and performance benefits. You can write Spin apps using a microservice-style design pattern, but without needing to deal with pointless network overhead or adding authentication to microservices that are internal-only.

But this idea is even more powerful when combined withe SpinKube’s ability to do Selective Deployments. With Selective Deployments, you can deploy some parts of your app to one deployment, and the other parts of the app to another deployment. That is, in our example above, you could choose to run the whole thing as one deployment, or you could (through configuration) run the Javascript REST API as one deployment and the Python frontend as another. In Kubernetes, that makes those two things independently routable, scalable, and configurable.

All of this is enabled by the WebAssembly Component Model, a powerful abstraction that allows different WebAssembly binaries (in this case, the JS app and the Python app) to communicate with each other. Spin 3 provides other interesting ways to compose components.

Even while these possibilities are exciting, in my view the performance and security improvements we saw here in this post are a good reason to build apps with local service chaining.

 

 

 


🔥 Recommended Posts


Quickstart Your Serverless Apps with Spin

Get Started