API server with FaunaDB
FaunaDB calls itself "The data API for modern applications". It's a database with a GraphQL interface that enables you to use GraphQL to interact with it. Since we communicate with it using HTTP requests, we don't need to manage connections which suits very well for serverless applications.
The tutorial assumes that you have FaunaDB and Deno Deploy accounts, Deno Deploy CLI installed, and some basic knowledge of GraphQL.
Overview
In this tutorial, let's build a small quotes API with endpoints to insert and retrieve quotes. And later use FaunaDB to persist the quotes.
Let's start by defining the API endpoints.
# A POST request to the endpoint should insert the quote to the list.
POST /quotes/
# Body of the request.
{
"quote": "Don't judge each day by the harvest you reap but by the seeds that you plant.",
"author": "Robert Louis Stevenson"
}
# A GET request to the endpoint should return all the quotes from the database.
GET /quotes/
# Response of the request.
{
"quotes": [
{
"quote": "Don't judge each day by the harvest you reap but by the seeds that you plant.",
"author": "Robert Louis Stevenson"
}
]
}
Now that we understand how the endpoint should behave, let's proceed to build it.
Build the API Endpoints
First, create a file named quotes.ts
and paste the following content.
Read through the comments in the code to understand what's happening.
import {
json,
serve,
validateRequest,
} from "https://deno.land/x/sift@0.6.0/mod.ts";
serve({
"/quotes": handleQuotes,
});
// To get started, let's just use a global array of quotes.
const quotes = [
{
quote: "Those who can imagine anything, can create the impossible.",
author: "Alan Turing",
},
{
quote: "Any sufficiently advanced technology is equivalent to magic.",
author: "Arthur C. Clarke",
},
];
async function handleQuotes(request: Request) {
// Make sure the request is a GET request.
const { error } = await validateRequest(request, {
GET: {},
});
// validateRequest populates the error if the request doesn't meet
// the schema we defined.
if (error) {
return json({ error: error.message }, { status: error.status });
}
// Return all the quotes.
return json({ quotes });
}
Run the above program using the Deno CLI.
deno run --allow-net=:8000 ./path/to/quotes.ts
# Listening on http://0.0.0.0:8000/
And curl the endpoint to see some quotes.
curl http://127.0.0.1:8000/quotes
# {"quotes":[
# {"quote":"Those who can imagine anything, can create the impossible.", "author":"Alan Turing"},
# {"quote":"Any sufficiently advanced technology is equivalent to magic.","author":"Arthur C. Clarke"}
# ]}
Let's proceed to handle the POST request.
Update the validateRequest
function to make sure a POST request follows the
provided body scheme.
- const { error } = await validateRequest(request, {
+ const { error, body } = await validateRequest(request, {
GET: {},
+ POST: {
+ body: ["quote", "author"]
+ }
});
Handle the POST request by updating handleQuotes
function with the following
code.
async function handleQuotes(request: Request) {
const { error, body } = await validateRequest(request, {
GET: {},
POST: {
body: ["quote", "author"],
},
});
if (error) {
return json({ error: error.message }, { status: error.status });
}
+ // Handle POST requests.
+ if (request.method === "POST") {
+ const { quote, author } = body as { quote: string; author: string };
+ quotes.push({ quote, author });
+ return json({ quote, author }, { status: 201 });
+ }
return json({ quotes });
}
Let's test it by inserting some data.
curl --dump-header - --request POST --data '{"quote": "A program that has not been tested does not work.", "author": "Bjarne Stroustrup"}' http://127.0.0.1:8000/quotes
The output might look like something below.
HTTP/1.1 201 Created
transfer-encoding: chunked
content-type: application/json; charset=utf-8
{"quote":"A program that has not been tested does not work.","author":"Bjarne Stroustrup"}
Awesome! We built our API endpoint, and it's working as expected. Since the data is stored in memory, it will be lost after a restart. Let's use FaunaDB to persist our quotes.
Use FaunaDB for Persistence
Let's define our database schema using GraphQL Schema.
# We're creating a new type named `Quote` to represent a quote and its author.
type Quote {
quote: String!
author: String!
}
type Query {
# A new field in the Query operation to retrieve all quotes.
allQuotes: [Quote!]
}
Fauna has a graphql endpoint for its database, and it generates essential
mutations like create, update, delete for a data type defined in the schema. For
example, fauna will generate a mutation named createQuote
to create a new
quote in the database for the data type Quote
. And we're additionally defining
a query field named allQuotes
that returns all the quotes in the database.
Let's get to writing the code to interact with fauna from Deno Deploy applications.
To interact with fauna, we need to make a POST request to its graphql endpoint with appropriate query and parameters to get the data in return. So let's construct a generic function that will handle those things.
async function queryFauna(
query: string,
variables: { [key: string]: unknown },
): Promise<{
data?: any;
error?: any;
}> {
// Grab the secret from the environment.
const token = Deno.env.get("FAUNA_SECRET");
if (!token) {
throw new Error("environment variable FAUNA_SECRET not set");
}
try {
// Make a POST request to fauna's graphql endpoint with body being
// the query and its variables.
const res = await fetch("https://graphql.fauna.com/graphql", {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify({
query,
variables,
}),
});
const { data, errors } = await res.json();
if (errors) {
// Return the first error if there are any.
return { data, error: errors[0] };
}
return { data };
} catch (error) {
return { error };
}
}
Add this code to the quotes.ts
file. Now let's proceed to update the endpoint
to use fauna.
async function handleQuotes(request: Request) {
const { error, body } = await validateRequest(request, {
GET: {},
POST: {
body: ["quote", "author"],
},
});
if (error) {
return json({ error: error.message }, { status: error.status });
}
if (request.method === "POST") {
+ const { quote, author, error } = await createQuote(
+ body as { quote: string; author: string }
+ );
+ if (error) {
+ return json({ error: "couldn't create the quote" }, { status: 500 });
+ }
return json({ quote, author }, { status: 201 });
}
return json({ quotes });
}
+async function createQuote({
+ quote,
+ author,
+}: {
+ quote: string;
+ author: string;
+}): Promise<{ quote?: string; author?: string; error?: string }> {
+ const query = `
+ mutation($quote: String!, $author: String!) {
+ createQuote(data: { quote: $quote, author: $author }) {
+ quote
+ author
+ }
+ }
+ `;
+
+ const { data, error } = await queryFauna(query, { quote, author });
+ if (error) {
+ return { error };
+ }
+
+ return data;
+}
Now that we've updated the code to insert new quotes let's set up a fauna database before proceeding to test the code.
Create a new database:
- Go to https://dashboard.fauna.com (login if required) and click on New Database
- Fill the Database Name field and click on Save.
- Click on GraphQL section visible on the left sidebar.
- Create a file ending with
.gql
extension with the content being the schema we defined above.
Generate a secret to access the database:
- Click on Security section and click on New Key.
- Select Server role and click on Save. Copy the secret.
Let's now run the application with the secret.
FAUNA_SECRET=<the_secret_you_just_obtained> deno run --allow-net=:8000 --watch quotes.ts
# Listening on http://0.0.0.0:8000
curl --dump-header - --request POST --data '{"quote": "A program that has not been tested does not work.", "author": "Bjarne Stroustrup"}' http://127.0.0.1:8000/quotes
Notice how the quote was added to your collection in FaunaDB.
Let's write a new function to get all the quotes.
async function getAllQuotes() {
const query = `
query {
allQuotes {
data {
quote
author
}
}
}
`;
const {
data: {
allQuotes: { data: quotes },
},
error,
} = await queryFauna(query, {});
if (error) {
return { error };
}
return { quotes };
}
And update the handleQuotes
function with the following code.
-// To get started, let's just use a global array of quotes.
-const quotes = [
- {
- quote: "Those who can imagine anything, can create the impossible.",
- author: "Alan Turing",
- },
- {
- quote: "Any sufficiently advanced technology is equivalent to magic.",
- author: "Arthur C. Clarke",
- },
-];
async function handleQuotes(request: Request) {
const { error, body } = await validateRequest(request, {
GET: {},
POST: {
body: ["quote", "author"],
},
});
if (error) {
return json({ error: error.message }, { status: error.status });
}
if (request.method === "POST") {
const { quote, author, error } = await createQuote(
body as { quote: string; author: string },
);
if (error) {
return json({ error: "couldn't create the quote" }, { status: 500 });
}
return json({ quote, author }, { status: 201 });
}
+ // It's assumed that the request method is "GET".
+ {
+ const { quotes, error } = await getAllQuotes();
+ if (error) {
+ return json({ error: "couldn't fetch the quotes" }, { status: 500 });
+ }
+
+ return json({ quotes });
+ }
}
curl http://127.0.0.1:8000/quotes
You should see all the quotes we've inserted into the database. The final code of the API is available at https://deno.com/examples/fauna.ts.
Deploy the API
Now that we have everything in place, let's deploy your new API!
- In your browser, visit Deno Deploy and link your GitHub account.
- Select the repository which contains your new API.
- You can give your project a name or allow Deno to generate one for you
- Select
index.ts
in the Entrypoint dropdown - Click Deploy Project
In order for your Application to work, we will need to configure its environment variables.
On your project's success page, or in your project dashboard, click on Add
environmental variables. Under Environment Variables, click + Add
Variable. Create a new variable called FAUNA_SECRET
- The value should be
the secret we created earlier.
Click to save the variables.
On your project overview, click View to view the project in your browser,
add /quotes
to the end of the url to see the content of your FaunaDB.