Bindable Microservices with Cloudflare Workers

Friday October 25, 2024

Bindable Microservices with Cloudflare Workers

Segment your application logic into feature specific services for a more scaleable architecture.

⋅.˳˳.⋅ॱ˙˙ॱ⋅.˳˳.⋅ॱ˙˙ॱ⋅.˳˳.⋅.


In the early stages of building any application, monolithic architectures can seem like the simplest solution. Everything contained in one code base. But as your application begins to scale, the monolithic approach starts to lead to more problems than it solves. Changes become riskier, deployments slower, and maintenance can turn into a nightmare as new features and fixes can inadvertently break existing functionality. In many instances, developers turn to a microservice architecture where managing a system of smaller, independent services, that work together takes away many risks and provides a lot of upside.

Enter Cloudflare Workers – a powerful serverless platform that not only simplifies creating and managing microservices but also allows them to be seamlessly bound together. In this blog, we’ll explore how you can leverage Cloudflare Workers to build a modern microservice architecture, complete with edge computing benefits and low-latency communication.

Above we show an example of what a commerce platform microservice architecture might look like. You have a "Core Service” where maybe some of the routing functionality exists and calls out to the number of other services that exist for them to handle their specialized functionality.

When a developer or team is making adjustments to a service such as User Auth, that team can feel more confidence in their own deployment cycle when it is more standalone and does not involve the entire application stack. So let’s go ahead and dive in.

Microservices in StarbaseDB

Before we dive into building our own microservice architecture with Cloudflare Workers I wanted to take a moment to touch on both why and how we use it in StarbaseDB – to lead by example.

For those who don’t know, StarbaseDB provides a lot of functionality that automatically supercharges your database. Out of the box you get a SQLite database, an interface to view your database, REST API, web socket communication, data importing & exporting, and much more. The goal of StarbaseDB is to eliminate as many of the development challenges that exist near the database for developers – and sometimes that means tackling problems that affect some people, but not all.

That’s where offering a variety of smaller services as optional add-ons helps showcase how great this service based architecture can be. An example service for StarbaseDB is user authentication and providing a solution to allow users to sign up, login and have a session without you needing to write a single line of code. Why should we offer a service to do this out of the box instead of letting teams write it themselves? They can still do that of course! But because user sessions typically need authorized before continuing to query a database (and all of the session information likely lives in the database anyways) there is an opportunity for us to help gate keep that access to the database for those looking to get jumpstarted quickly.

We’re big believers that micro services help teams stay lean, iterate quickly, maintain focus, and ship fast.

See examples of our services in the Pull Requests section at the bottom of this blog.

Project Setup

For this to work we need to create 2 separate Workers. One will be our main component, while the second will serve as our sample service. In this example our main service will serve as a router mechanism that listens for incoming routes and then takes traffic to another service to handle feature-specific logic.

Roll up your sleeves, open up your favorite IDE and let’s get started.

Create Main Component

We first need to create two folders at the root of our project that contains the code for each of the services we are going to program. Go ahead and create the following two folders at the root level of your project:

  • core - this will handle our incoming request routing

  • auth - this will be specifically for all auth functionality

Traditionally you’d likely use other tools to serve as an incoming request routing mechanism but without introducing too many concepts or unnecessary code I want to keep this simple so you can see how easy it truly is.

With our two folders created, go ahead and create the following three files inside of our core folder:

  • ./core/wrangler.toml

  • ./core/package.json

  • ./core/index.ts

Let’s take a look at what each file should contain as code.

wrangler.toml

name = "microservice_core"
main = "./index.ts"
compatibility_date = "2024-09-25"

[observability]
enabled = true

If you’re not familiar with Cloudflare’s Wrangler, the above file is used as metadata for our deployment to properly name or identify our service, and turn on some additional features (such as logging here). It also points to our main entry point file.

package.json

{
    "name": "microservice-core",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "deploy": "wrangler deploy",
        "dev": "wrangler dev",
        "start": "wrangler dev",
        "cf-typegen": "wrangler types"
    },
    "devDependencies": {
        "@cloudflare/workers-types": "^4.20240925.0",
        "typescript": "^5.5.2",
        "wrangler": "^3.60.3"
    }
}

Above is a template package.json file I use for starting with Cloudflare Workers. It provides the scripts to deploy our code with the proper dependencies.

index.ts

interface Env {

}

const jsonResponse = (data: unknown, status = 200): Response => {
    return new Response(JSON.stringify(data), {
        status,
        headers: {
            'Content-Type': 'application/json',
        },
    });
};

export default {
    async fetch(request: Request, env: Env): Promise<Response> {
        const url = new URL(request.url);
        const { pathname } = url;
        const method = request.method.toUpperCase();

        if (pathname === '/api/hello' && method === 'GET') {
            return jsonResponse({ message: 'Hello, World!' });
        }

        return jsonResponse({ error: 'Not Found' }, 404);
    }
};

Our index file above is a pretty lightweight Cloudflare Worker. The default export has a fetch(..) function available to us that listens to web traffic that reaches the destination of this Worker and allows us to implement custom logic to do what we want with those requests. In this instance we’re just supplying a test /api/hello route to make sure our Worker is accessible when we deploy it.

Create Service Component

Our second service component here is going to look slightly different when it comes to the code piece, but not by much. Now in our second root folder of auth we’ll create a similar file structure as we did above.

  • ./auth/wrangler.toml

  • ./auth/package.json

  • ./auth/index.ts

Again, let’s look at what the code for each file looks like.

wrangler.toml

name = "microservice_auth"
main = "./index.ts"
compatibility_date = "2024-09-25"

[observability]
enabled = true

package.json

{
    "name": "microservice-auth",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "deploy": "wrangler deploy",
        "dev": "wrangler dev",
        "start": "wrangler dev",
        "cf-typegen": "wrangler types"
    },
    "devDependencies": {
        "@cloudflare/workers-types": "^4.20240925.0",
        "typescript": "^5.5.2",
        "wrangler": "^3.60.3"
    }
}

index.ts

import { WorkerEntrypoint } from "cloudflare:workers";

const jsonResponse = (data: unknown, status = 200): Response => {
    return new Response(JSON.stringify(data), {
        status,
        headers: {
            'Content-Type': 'application/json',
        },
    });
};

export default class AuthEntrypoint extends WorkerEntrypoint {
    async fetch() { return new Response(null, {status: 404}); }

    async handleAuthRequest(url: string, method: string) {
        if (url === '/api/login' && method === 'GET') {
            return jsonResponse({ message: 'You reached the auth microservice!' });
        }

        return new Response(null, {status: 404});
    }
}

The first two files are nearly identical with just name changes, but the last file has a default class that extends WorkerEntrypoint. What the WorkerEntrypoint class allows is for our core service to be able to identify this deployment by its class name which we cover in the next step. But similarly to our first class we can start writing whatever application level logic we want and return any response back to the user.

Binding Services in Workers

If we haven’t already let’s go ahead and deploy both of our services to Cloudflare. Inside each of our two root folders, core and auth, we can run a command line from our Terminal to npm i && npm run cf-typegen && npm run deploy which should walk you through deploying to your connected Cloudflare account.

With both of our services now deployed we need to find a way to connect them. As it stands they are just two isolated, independent services unknowing that the other even exists.

Add Service to Wrangler

Going back to our core service we need to tell it that there is another service it can begin to reference and know about. Open up the ./core/wrangler.toml file and add the following lines of code to the end of the file.

[[services]]
binding = "AUTH" # How we want to reference it
service = "microservice_auth" # Name from our services wrangler
entrypoint = "AuthEntrypoint" # Class name

What this tells our Worker is that it should be able to see another Worker called microservice_auth in the same Cloudflare account, and now it knows both what to call it internally, AUTH, and how to hit the entry point of that file – our AuthEntrypoint class.

Update our Env

With the wrangler file updated, you can now execute the following command again to update our generated file worker-configuration.d.ts that shows we have access to those types now.

npm run cf-typegen

Now in our ./core/index.ts file we can update the Env interface previously emptily declared at the top of our file so now the rest of our file knows how to call properly into this new entry point.

interface Env {
    AUTH: {
        handleAuthRequest(url: string, method: string): Promise<any>;
    }
}

Direct Traffic to Service

You might be happy to hear that everything is now in place. Now, all we need to do is direct traffic to our auth service when we see a user hitting our Worker URL. We already handled in our ./core/index.ts file when a user would hit our Worker with the URL https://microservice_core.YOUR-IDENTIFIER.workers.dev/api/hello the browser would print out a JSON message of “Hello, World!”.

Now we want to allow users to also access the following URL path and let our auth service handle how it should respond.

if (pathname === '/api/hello' && method === 'GET') {
    return jsonResponse({ message: 'Hello, World!' });
} else if (pathname.startsWith('/api/auth')) {
    return await env.AUTH.handleAuthRequest(pathname, request.method);
}

After we make these changes all we need is one final deployment of our core service! So let’s go ahead and run the following command inside our ./core folder:

npm run cf-typegen && npm run deploy

Now you can visit the following URL’s on your browser and see both services in action. You can get your URL from the message output of your npm run deploy command to replace with what I have below, but the paths should remain the same at the end.

Core: https://microservice_core.YOUR-IDENTIFIER.workers.dev/api/hello
Auth: https://microservice_core.YOUR-IDENTIFIER.workers.dev/api/auth/login

Conclusion

There is something absolutely wild about being able to deploy a custom backend to the internet in 3 files and with 1 command. It allows us to create each piece of functionality that should be a siloed process, as its own deployment. As we have shown, it can be done without headaches, planning or preparation – you can just do it.

If you anticipate your project growing to any size where greater than a handful of engineers will end up becoming involved I would highly encourage you to try starting your next project with a microservice architecture early. You’ll reap the benefits later when you are not playing whack-a-mole with bugs as multiple engineers are touching lines of code they think but are not certain should only impact their code and nobody else.

Pull Requests

Want to see the code that was contributed alongside this article? Check the contributions out below!

https://github.com/Brayden/starbasedb/pull/26 (User Authentication)

https://github.com/Brayden/starbasedb/pull/27 (Dynamic Data Masking)

Join the Adventure

We’re working on building an open-source database offering with building blocks we talk about above and more. Our goal is to help make database interactions easier, faster, and more accessible to every developer on the planet. Follow us, star us, and check out Outerbase below!

Twitter: https://twitter.com/BraydenWilmoth

Github Repo: github.com/Brayden/starbasedb

Outerbase: https://outerbase.com