Serverless GraphQL with FaunaDB and ZEIT Now
Getting started with databases and GraphQL has really never been easier. Previously I’ve gone through Wes Bos’s Advanced React and GraphQL course and it was great despite the fact that I was not able to apply the backend knowledge professionally. My background is with relational databases such as Microsoft SQL and Oracle SQL on top of others like MySQL and MariaDB, and since I’ve only dabbled with MongoDB Atlas I decided to give another NoSQL database a try. Ultimately I ended up with FaunaDB, but it’s not quite a NoSQL database, it’s something… nicer.
The database built for serverless, featuring native GraphQL
FaunaDB
We’re looking to use GraphQL and don’t want to host a database or a server ourselves. We want to focus on code.
The previously mentioned course from Wes Bos featured an Express GraphQL server with GraphQL Yoga and Prisma to build our schema with fancy mutations, resolvers, and the like. Although it was fun to see some of the grit, running our own server somewhere wasn’t ideal. Fauna fills this role as our serverless database provider, which means we only have to call on it.
Let’s take a stroll through setting up a database with FaunaDB, writing and importing a schema into the new database, and taking a look at the GraphQL endpoint to see how we can perform queries and mutations generated by Fauna.
To get started, go ahead and sign up for a Fauna account.
GraphQL
For this demo we are going to take a look at setting up a generic schema for users of an application. Create a new project directory and let’s create our GraphQL schema file:
# /schema.graphql
# create an object to store user data
type User {
# property `name` will be a String
name: String!
}
# create queries to search for all users
# and for user by name
type Query {
allUsers: [User]!
user(name: String): User
}
After taking the tour of FaunaDB, head on over to create a new database - demo
- and import the new GraphQL schema to our Fauna database.
Once Fauna is done importing the schema use the playground to poke around at what has been generated for us to consume like createUser
. All of our CRUD operations have been generated for us, and all we have to do is perform operations via GraphQL queries and mutations.
When the playground loads you’ll notice it’s pointing to https://graphql.fauna.com/graphql
and has a header with Basic authentication, which is a base64 encoded string of username:password
. It’s good to know that it is authenticated, however we don’t want to use our Fauna credentials to access the Fauna database so we’ll need a token.
Head on over to the “Security” tab.
Give it a name like “serverless-demo” or something that makes sense to you, then hold on to that key. Create a new file, .env
:
DB_TOKEN=<fauna-security-token>
Perfect. Let’s head on over to set up our serverless project with ZEIT Now.
ZEIT Now
In our project directory, create a new ZEIT Now project:
now
Follow the prompts and continue on through accepting the defaults or changing the name. Create a new directory named api
and spin up the new project with yarn init -y
, after it should look something like:
| my-new-serverless-project
|- api/
|- schema.graphql
|- .env
|- package.json
In the package.json
file let’s include a few dependencies and install them with yarn
"dependencies": {
"apollo-link-http": "^1.5.16",
"apollo-server-micro": "^2.11.0",
"graphql": "^14.6.0",
"isomorphic-unfetch": "^3.0.0"
}
Before moving forward, remember how we created the environment file, .env
, and populated our DB_TOKEN
variable? We’ll need to do something similar for Now to expose that variable to our API routes at runtime. This will be done in two steps:
- Add secret to Now
- Map secret to (local) environment variable
In your terminal,
now secrets add db_token <fauna-security-token>
Here we’re defining the secret all lowercase, even if we defined it like we did our environment variable DB_TOKEN
ZEIT Now changes it to the lowercase version, db_token
. Now that we’ve got our secret added, let’s map the secret by creating a new file in the project root, now.json
:
{
"env": {
"DB_TOKEN": "@db_token"
}
}
With our environment variable stored on ZEIT Now and mapped for runtime exposure our API routes can now consume the security token both locall with now dev
and when deployed.
Apollo
From my brief time with the Fauna community and seeing other’s interpretations, the Apollo layer is not entirely necessary as you can connect to Fauna using the bearer token directly. The benefits of Apollo are that it allows you to integrate third party security and write additional resolvers to the connected application.
Let’s start by creating a new file in the api/
directory, graphql.js
, to create our Apollo Link:
The http link is a terminating link that fetches GraphQL results from a GraphQL endpoint over an http connection. The http link supports both POST and GET requests with the ability to change the http options on a per query basis. This can be used for authentication, persisted queries, dynamic uris, and other granular updates.
// api/graphql.js
import { createHttpLink } from "apollo-link-http"
import fetch from "isomorphic-unfetch"
const link = createHttpLink({
uri: "https://graphql.fauna.com/graphql",
fetch,
headers: {
Authorization: `Bearer ${process.env.DB_TOKEN}`,
},
})
This snippet creates a connection to our Fauna database, which will allow us to fetch our schema from the remote target (Fauna). Since we are getting our schema from Fauna there’s no need to add typedefs or resolvers, however if we were to add additional resolvers not already available in our Fauna instance the logic would need to be written with FQL – Fauna Query Language. In this demo we are going to avoid doing so.
Let’s set up our Apollo Server handler using the micro distribution:
// api/graphql.js
import { createHttpLink } from "apollo-link-http"
import {
ApolloServer,
makeRemoteExecutableSchema,
introspectSchema,
} from "apollo-server-micro"
import fetch from "isomorphic-unfetch"
// create Fauna link
const link = createHttpLink({
uri: "https://graphql.fauna.com/graphql",
fetch,
headers: {
Authorization: `Bearer ${process.env.DB_TOKEN}`,
},
})
let handler
const getHandler = async () => {
// do nothing if handler is still valid (function's container still running)
if (handler) return handler
// make remote schema usable with Apollo
const schema = makeRemoteExecutableSchema({
// fetch schema from remote host
schema: await introspectSchema(link),
// provide link
link,
})
// create Apollo Server with created schema
// `introspection: true` allows us to see the schema when we connect using GraphQL Playground
const server = new ApolloServer({ schema, introspection: true })
// manually applying `/api/graphql` for demonstration purposes
handler = server.createHandler({ path: "/api/graphql" })
return handler
}
export default async (req, res) => {
const handler = await getHandler()
await handler(req, res)
}
There’s a lot going on with this update so let’s break it down by first taking a look at the very end, the default export of our file. ZEIT’s emulated Express handler provides a familiar implementation pattern. This is where we’ll use the Apollo handler.
Inside the getHandler
function we are fetching our schema from Fauna and making it usable with Apollo, then creating the server instance with the ability to browse the schema when connecting using something like GraphQL Playground.
Another small item I want to point out is the initialization of our handler
variable and the first line inside the getHandler
function:
// do nothing if handler is still valid (function's container still running)
if (handler) return handler
At first you may be thinking if this function runs once when its called and exits, why is this necessary? Well, our functions are running on micro which is a ZEIT distribution for “Asynchronous HTTP microservices”; kind of like their own tiny version of Express built for small, containerized microservices. On Now, these functions are spun up in a container when called upon, and may still be active when subsequently called, thus our handler
would still have an assigned value given our container hasn’t exited.
Finally coming back to the default export, we’re using our handler to pass in requests making the Apollo Server more of a middleware to our Fauna database.
We can go ahead and test out our work:
now dev
This will run the Now development environment, enabling us to replicate the production environment locally to run our serverless functions.
Results
Open GraphQL Playground and plug in the localhost
URL appended with /api/graphql
. You should now be able to perform queries, mutations, and browse the available schema. Awesome! But there is concern with the security. We’re using our security token from Fauna to access the database with Apollo, but nothing is in place to prevent people from calling the publicly available ZEIT Now API.
Moving forward try to leverage Apollo Server’s context to add the authentication check to block unauthorized calls. Next time we’ll take a look at using Auth0 with Svelte to protect our API while also creating the frontend.