A few months ago Cloudflare launched Custom Domains into an open beta. Custom Domains allow you to hook up your Workers to the Internet, without having to deal with DNS records or certificates ā just enter a valid hostname and Cloudflare will do the rest! The betaās over, and Custom Domains are now GA.
Custom Domains arenāt just about a seamless developer experience; they also allow you to build a globally distributed instantly scalable application on Cloudflareās Developer Platform. Thatās because Workers leveraging Custom Domains have no concept of an āOrigin Serverā. Thereās no āhomeā to phone to - and that also means your application can use the poCloudflarer of Cloudflareās global network to run your application, Cloudflarell, everywhere. Itās truly serverless.
Ā
Letās build āTodoā, but without the servers
Today Iāll start a series of posts outlining a simple todo list application. Iāll start with an API and hook it up to the Internet using Custom Domains.
With Custom Domains, youāre treating the whole network as the application server. Any time a request comes into a Cloudflare data center, Workers are triggered in that data center and connect to resources across the network as needed. Our developers donāt need to think about regions, or replication, or spinning up the right number of instances to handle unforeseen load. Instead, just deploy your Workers and Cloudflare will handle the rest.
For our todo application, I begin by building an API Gateway to perform routing, any authorization checks, and drop invalid requests. Cloudflare then fan out to each individual use case in a separate Worker, so our teams can independently make updates or add features to each endpoint without a full redeploy of the whole application. Finally, each Worker has a D1 binding to be able to create, read, update, and delete records from the database. All of this happens on Cloudflareās global network, so your API is truly available everywhere. The architecture will look something like this:
Bootstrap the D1 Database
First off, weāre going to need a D1 database set up, with a schema for our todo application to run on. If youāre not familiar with D1, itās Cloudflareās serverless database offering - explained in more detailĀ here. To get started, we use theĀ
wrangler d1
Ā command to create a new database:npx wrangler d1 create <todo | custom-database-name>
After executing this command, you will be asked to add a snippet of code to yourĀ
wrangler.toml
Ā file that looks something like this:[[ d1_databases ]] binding = "db" # i.e. available in your Worker on env.db database_name = "<todo | custom-database-name>" database_id = "<UUID>"
Letās save that for now, and weāll put these into each of our private microservices in a few moments. Next, weāre going to create our database schema. Itās a simple todo application, so itāll look something like this, with some seeded data:
db/schema.sql
DROP TABLE IF EXISTS todos; CREATE TABLE todos (id INTEGER PRIMARY KEY, todo TEXT, todoStatus BOOLEAN NOT NULL CHECK (todoStatus IN (0, 1))); INSERT INTO todos (todo, todoStatus) VALUES ("Fold my laundry", 0),("Get flowers for mumās birthday", 0),("Find Nemo", 0),("Water the monstera", 1);
You can bootstrap your new D1 database by running:
npx wrangler d1 execute <todo | custom-database-name> --file=./schema.sql
Then validate your new data by running a query through Wrangler using the following command:
npx wrangler d1 execute <todo | custom-database-name> --command='SELECT * FROM todos';
Great! Weāve now got a database running entirely on Cloudflareās global network.
Build the endpoint Workers
To talk to your database, weāll spin up a series of private microservices for each endpoint in our application. We want to be able to create, read, update, delete, and list our todos. The full source code for each is availableĀ here. Below is code from a Worker that lists all our todos from D1.
list/src/list.js
export default { async fetch(request, env) { const { results } = await env.db.prepare( "SELECT * FROM todos" ).all(); return Response.json(results); }, };
The Worker ātodo-listā needs to be able to access D1 from the environment variableĀ
db
. To do this, weāll define the D1 binding in ourĀ wrangler.toml
Ā file. We also specify that workers_dev is false, preventing a preview from being generated via workers.dev (we want this to be aĀ privateĀ microservice).list/wrangler.toml
name = "todo-list" main = "src/list.js" compatibility_date = "2022-09-07" workers_dev = false usage_model = "unbound" [[ d1_databases ]] binding = "db" # i.e. available in your Worker on env.db database_name = "<todo | custom-database-name>" database_id = "UUID"
Finally, use wrangler publish to deploy this microservice.
todo/list on āmain [!] āŗ wrangler publish ā ļø wrangler 0.0.0-893830aa ----------------------------------------------------------------------- Retrieving cached values for account from ../../../node_modules/.cache/wrangler Your worker has access to the following bindings: - D1 Databases: - db: todo (UUID) Total Upload: 4.71 KiB / gzip: 1.60 KiB Uploaded todo-list (0.96 sec) No publish targets for todo-list (0.00 sec)
Notice that wrangler mentions there are no āpublish targetsā for todo-list. Thatās because we havenāt hooked todo-list up to any HTTP endpoints. Thatās fine! Weāre going to use Service Bindings to route requests through a gateway worker, as described in the architecture diagram above.
Next, reuse these steps to create similar microservices for each of our create, read, update, and delete endpoints. The source code is available toĀ follow along.
Tying it all together with an API Gateway
Each of our Workers are able to talk to the D1 database, but how can our application talk to our API? Weāll build out a simple API gateway to route incoming requests to the appropriate microservice. For the purposes of our application, weāre using a combination of URL pathname and request method to detect which endpoint is appropriate.
gateway/src/gateway.js
export default { async fetch(request, env) { try{ const url = new URL(request.url) const idPattern = new URLPattern({ pathname: '/:id' }) if (idPattern.test(request.url)) { switch (request.method){ case 'GET': return await env.get.fetch(request.clone()) case 'PATCH': return await env.update.fetch(request.clone()) case 'DELETE': return await env.delete.fetch(request.clone()) default: return new Response("Unsupported method for endpoint /:id", {status: 405}) } } else if (url.pathname == '/') { switch (request.method){ case 'GET': return await env.list.fetch(request.clone()) case 'POST': return await env.create.fetch(request.clone()) default: return new Response("Unsupported method for endpoint /", {status: 405}) } } return new Response("Not found. Supported endpoints are /:id and /", {status: 404}) } catch(e) { return new Response(e, {status: 500}) } }, };
With our API gateway all set, we just need to expose our application to the Internet using a Custom Domain, and hook up our Service Bindings, so the gateway Worker can access each appropriate microservice. Weāll set this up in aĀ
wrangler.toml
.gateway/wrangler.toml
name = "todo-gateway" main = "src/gateway.js" compatibility_date = "2022-09-07" workers_dev = false usage_model = "unbound" routes = [ {pattern="todos.radiobox.tv", custom_domain=true, zone_name="radiobox.tv"} ] services = [ {binding = "get",service = "todo-get"}, {binding = "delete",service = "todo-delete"}, {binding = "create",service = "todo-create"}, {binding = "update",service = "todo-update"}, {binding = "list",service = "todo-list"} ]
Next, useĀ
wrangler publish
Ā to deploy your application to the Cloudflare network. Seconds later, youāll have a simple, functioning todo API built entirely on Cloudflareās Developer Platform!āŗ wrangler publish ā ļø wrangler 0.0.0-893830aa ----------------------------------------------------------------------- Retrieving cached values for account from ../../../node_modules/.cache/wrangler Your worker has access to the following bindings: - Services: - get: todo-get - delete: todo-delete - create: todo-create - update: todo-update - list: todo-list Total Upload: 1.21 KiB / gzip: 0.42 KiB Uploaded todo-gateway (0.62 sec) Published todo-gateway (0.51 sec) todos.radiobox.tv (custom domain - zone name: radiobox.tv)
Natively Global
Since itās built natively on Cloudflare, you can also include Cloudflareās security suite in front of the application. If we want to prevent SQL Injection attacks for this endpoint, we can enable the appropriate Managed WAF rules on our todos API endpoint. Alternatively, if we wanted to prevent global access to our API (only allowing privileged clients to access the application), we can simply put Cloudflare Access in front, with custom Access Rules.
With Custom Domains on Workers, youāre able to easily create applications that are native to Cloudflareās global network, instantly. Best of all, your developers donāt need to worry about maintaining DNS records or certificate renewal - Cloudflare handles it all on their behalf. Weād like to give a huge shout out to the 5,000+ developers who used Custom Domains during the open beta period, and those that gave feedback along the way to make this possible. Canāt wait to see what you build next! As always, if you have any questions or would like to get involved, please join us onĀ Discord.
Tune in next time to see how we can build a frontend for our application. In the meantime, you can play around with the todos API we built today atĀ
todos.radiobox.tv
.Ā
Blog Article by:
Ankur Paul