Create a serverless, globally distributed REST API with Fauna
In this tutorial, you learn how to store and retrieve data in your Cloudflare Workers applications by building a REST API that manages an inventory catalog using Fauna as its data layer.
Learning goals
- How to store and retrieve data from Fauna in Workers.
- How to use Wrangler to store secrets securely.
- How to use Hono as a web framework for your Workers.
Building with Fauna, Workers, and Hono enables you to create a globally distributed, strongly consistent, fully serverless REST API in a single repository.
Fauna is a document-based database with a flexible schema. This allows you to define the structure of your data – whatever it may be – and store documents that adhere to that structure. In this tutorial, you will build a product inventory, where each product
document must contain the following properties:
- title - A human-friendly string that represents the title or name of a product.
- serialNumber - A machine-friendly string that uniquely identifies the product.
- weightLbs - A floating point number that represents the weight in pounds of the product.
- quantity A non-negative integer that represents how many items of a particular product there are in the inventory.
Documents are stored in a collection. Collections in document databases are groups of related documents.
For this tutorial, all API endpoints are public. However, Fauna also offers multiple avenues for securing endpoints and collections. Refer to Choosing an authentication strategy with Fauna for more information on authenticating users to your applications with Fauna.
Before you start
All of the tutorials assume you have already completed the Get started guide, which gets you set up with a Cloudflare Workers account, C3, and Wrangler.
Set up Fauna
Create your database
Open the Fauna dashboard in your browser and log in to your Fauna account.
In the Fauna dashboard:
- Select CREATE DATABASE.
- Provide a valid name.
- Select a Region Group.
- Select CREATE.
Create a collection
To create a collection named Products, enter the FQL query in the SHELL window on right side of the screen.
Create a new collectionCollection.create({ name: "Products" })
Select Run. You will see an output similar to the following.
Output{ name: "Products", coll: Collection, ts: "<timestamp>", indexes: {}, constraints: []
}
Create a secret key
You must create a secret key to connect to the database from your Worker.
To create a secret key:
- Go to Explorer in the Fauna dashboard.
- Hover over your database name, and select the key icon to manage your keys.
- Choose Server Role and enter a key name.
The Fauna dashboard displays the key’s secret. Copy and save this server key to use in a later step.
Manage your inventory with Workers
Create a new Worker project
Create a new project by using C3.
Create a new project$ npm create cloudflare@latest
Create a new project$ yarn create cloudflare
To continue with this guide:
- Give your new Worker application a name.
- Select
Website or web app
. - Select
Hono
. - Select
No
to skip Git initialization. - Select
No
to skip deploying your application.
Next, go to your Worker project directory and update the wrangler.toml
file to set the name for the Worker.
wrangler.tomlname = "fauna-workers"
Add your Fauna secret key as a secret
Before developing your Worker, add your Fauna secret key as a secret.
There are two types of secrets for development or production.
For development, add a .dev.vars
file on the project root and write your secret.
.dev.varsFAUNA_SECRET=<YOUR SECRET>
For production, store your secret safely with wrangler secret put
command:
Store your Fauna secret$ npx wrangler secret put FAUNA_SECRET
When prompted, paste the Fauna server secret you obtained earlier.
The FAUNA_SECRET
secret is now injected automatically into your Worker code at runtime.
Install dependencies
Install the Fauna JavaScript driver in your newly created Worker project.
Install the Fauna driver$ npm install fauna
Install the Fauna driver$ yarn add fauna
Base inventory logic
Replace the contents of your src/index.ts
file with the skeleton of your API:
src/index.tsimport { Hono } from 'hono';
import { Client, fql, ServiceError } from 'fauna';
type Bindings = { FAUNA_SECRET: string;
};
type Variables = { faunaClient: Client;
};
type Product = { id: string; serialNumber: number; title: string; weightLbs: number; quantity: number;
};
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>();
app.use('*', async (c, next) => { const faunaClient = new Client({ secret: c.env.FAUNA_SECRET, }); c.set('faunaClient', faunaClient); await next();
});
app.get('/', (c) => { return c.text('Hello World');
});
export default app;
This is custom middleware to initialize the Fauna client and set the instance with c.set()
for later use in another handler:
Custom middleware for the Fauna Clientapp.use('*', async (c, next) => { const faunaClient = new Client({ secret: c.env.FAUNA_SECRET, }); c.set('faunaClient', faunaClient); await next();
});
You can access the FAUNA_SECRET
environment variable from c.env.FAUNA_SECRET
. Workers run on a custom JavaScript runtime instead of Node.js, so you cannot use process.env
to access your environment variables.
Create product documents
Add your first Hono handler to the src/index.ts
file. This route accepts POST
requests to the /products
endpoint:
Create product documentsapp.post('/products', async (c) => { const { serialNumber, title, weightLbs } = await c.req.json<Omit<Product, 'id'>>(); const query = fql`Products.create({ serialNumber: ${serialNumber}, title: ${title}, weightLbs: ${weightLbs}, quantity: 0 })`; const result = await c.var.faunaClient.query<Product>(query); return c.json(result.data);
});
This route applied an FQL query in the fql
function that creates a new document in the Products collection:
Create query in FQL inside JavaScriptfql`Products.create({ serialNumber: ${serialNumber}, title: ${title}, weightLbs: ${weightLbs}, quantity: 0})`
To review what a document looks like, run the following query. In the Fauna dashboard, go to Explorer > Region name > Database name like a cloudflare_rest_api
> the SHELL window:
Create query in pure FQLProducts.create({ serialNumber: "A48432348", title: "Gaming Console", weightLbs: 5, quantity: 0
})
Fauna returns the created document:
Newly created document{ id: "<document_id>", coll: Products, ts: "<timestamp>", serialNumber: "A48432348", title: "Gaming Console", weightLbs: 5, quantity: 0
}
Examining the route you create, when the query is successful, the data newly created document is returned in the response body:
Return the new document datareturn c.json({ productId: result.data,
});
Error handling
If Fauna returns any error, an exception is raised by the client. You can catch this exception in app.onError()
, then retrieve and respond with the result from the instance of ServiceError
.
Handle errorsapp.onError((e, c) => { if (e instanceof ServiceError) { return c.json( { status: e.httpStatus, code: e.code, message: e.message, }, e.httpStatus ); } console.trace(e); return c.text('Internal Server Error', 500);
});
Retrieve product documents
Next, create a route that reads a single document from the Products collection.
Add the following handler to your src/index.ts
file. This route accepts GET
requests at the /products/:productId
endpoint:
Retrieve product documentsapp.get('/products/:productId', async (c) => { const productId = c.req.param('productId'); const query = fql`Products.byId(${productId})`; const result = await c.var.faunaClient.query<Product>(query); return c.json(result.data);
});
The FQL query uses the byId()
method to retrieve a full document from the Productions collection:
Retrieve a document by ID in FQL inside JavaScriptfql`Products.byId(productId)`
If the document exists, return it in the response body:
Return the document in the response bodyreturn c.json(result.data);
If not, an error is returned.
Delete product documents
The logic to delete product documents is similar to the logic for retrieving products. Add the following route to your src/index.ts
file:
Delete product documentsapp.delete('/products/:productId', async (c) => { const productId = c.req.param('productId'); const query = fql`Products.byId(${productId})!.delete()`; const result = await c.var.faunaClient.query<Product>(query); return c.json(result.data);
});
The only difference from the previous route is that you use the delete()
method, combined with the byId()
method, to delete a document.
When the delete operation is successful, Fauna returns the deleted document and the route forwards the deleted document in the response’s body. If not, an error is returned.
Test and deploy your Worker
Before deploying your Worker, test it locally by using Wrangler’s dev
command:
Develop your Worker$ npm run dev
Develop your Worker$ yarn dev
Once the development server is up and running, start making HTTP requests to your Worker.
First, create a new product:
Create a new product$ curl \
--data '{"serialNumber": "H56N33834", "title": "Bluetooth Headphones", "weightLbs": 0.5}' \ --header 'Content-Type: application/json' \ --request POST \ http://127.0.0.1:8787/products
You should receive a 200
response similar to the following:
Create product response{ "productId": "<document_id>"
}
Next, read the document you created:
Read a document$ curl \
--header 'Content-Type: application/json' \ --request GET \ http://127.0.0.1:8787/products/<document_id>
The response should be the new document serialized to JSON:
Read product response{ "coll": { "name": "Products" }, "id": "<document_id>", "ts": { "isoString": "<timestamp>" }, "serialNumber": "H56N33834", "title": "Bluetooth Headphones", "weightLbs": 0.5, "quantity": 0
}
Finally, deploy your Worker using the wrangler deploy
command:
Deploy your Worker$ npm run deploy
Deploy your Worker$ yarn deploy
This publishes the Worker to your *.workers.dev
subdomain.
Update inventory quantity
As the last step, implement a route to update the quantity of a product in your inventory, which is 0
by default.
This will present a problem. To calculate the total quantity of a product, you first need to determine how many items there currently are in your inventory. If you solve this in two queries, first reading the quantity and then updating it, the original data might change.
Add the following route to your src/index.ts
file. This route responds to HTTP PATCH
requests on the /products/:productId/add-quantity
URL endpoint:
Update inventory quantityapp.patch('/products/:productId/add-quantity', async (c) => { const productId = c.req.param('productId'); const { quantity } = await c.req.json<Pick<Product, 'quantity'>>(); const query = fql`Products.byId(${productId}){ quantity : .quantity + ${quantity}}`; const result = await c.var.faunaClient.query<Pick<Product, 'quantity'>>(query); return c.json(result.data);
});
Examine the FQL query in more detail:
Update query in FQL inside JavaScriptfql`Products.byId(${productId}){ quantity : .quantity + ${quantity}}`;
Test your update route:
Update product inventory$ curl \
--data '{"quantity": 5}' \ --header 'Content-Type: application/json' \ --request PATCH \ http://127.0.0.1:8787/products/<document_id>/add-quantity
The response should be the entire updated document with five additional items in the quantity:
Update product response{ "quantity": 5
}
Update your Worker by deploying it to Cloudflare.
Update your Worker in Cloudflare$ npm run deploy
Update your Worker in Cloudflare$ yarn deploy
Related resources
In this tutorial, you learned how to use Fauna with Cloudflare Workers to create a globally distributed, strongly consistent, next-generation serverless REST API that serves data quickly to a worldwide audience.
If you would like to review the full source code for this application, you can find the repository on GitHub.