FaunaDB is a serverless global database designed for low latency and developer productivity. It has proven to be particularly appealing to Jamstack developers for its global scalability, native GraphQL API, and FQL query language.
Being a serverless distributed database, the JAMstack world is a natural fit for our system, but long before we were chasing JAMstack developers, we were using the stack ourselves.
Matt Attaway
Lessons Learned Livin’ La Vida JAMstack (January 24, 2020)
In this post, we will walk through how to create an application with RedwoodJS and FaunaDB.
Redwood Monorepo
Create Redwood App
To start we’ll create a new Redwood app from scratch with the Redwood CLI. If you don’t have yarn installed enter the following command:
npm install -g yarn
Now we’ll use yarn create redwood-app
to generate the basic structure of our app.
yarn create redwood-app ./redwood-fauna
I’ve called my project redwood-fauna
but feel free to select whatever name you want for your application. We’ll now cd
into our new project and use yarn rw dev
to start our development server.
cd redwood-fauna
yarn rw dev
Our project’s frontend is running on localhost:8910
and our backend is running on localhost:8911
ready to receive GraphQL queries.
Redwood Directory Structure
One of Redwood’s guiding philosophies is that there is power in standards, so it makes decisions for you about which technologies to use, how to organize your code into files, and how to name things.
It can be a little overwhelming to look at everything that’s already been generated for us. The first thing to pay attention to is that Redwood apps are separated into two directories:
api
for backendweb
for frontend
├── api
│ ├── db
│ │ ├── schema.prisma
│ │ └── seeds.js
│ └── src
│ ├── functions
│ │ └── graphql.js
│ ├── graphql
│ ├── lib
│ │ └── db.js
│ └── services
└── web
├── public
│ ├── favicon.png
│ ├── README.md
│ └── robots.txt
└── src
├── components
├── layouts
├── pages
├── FatalErrorPage
│ └── FatalErrorPage.js
└── NotFoundPage
└── NotFoundPage.js
├── index.css
├── index.html
├── index.js
└── Routes.js
Each side has their own path in the codebase. These are managed by Yarn workspaces. We will be talking to the Fauna client directly so we can delete the db
directory along with the files inside it and we can delete all the code in db.js
.
Pages
With our application now set up we can start creating pages. We’ll use the generate page
command to create a home page and a folder to hold that page. Instead of generate
we can use g
to save some typing.
yarn rw g page home /
If we go to our web/src/pages
directory we’ll see a HomePage
directory containing this HomePage.js
file:
// web/src/pages/HomePage/HomePage.js
import { Link } from '@redwoodjs/router'
const HomePage = () => {
return (
<>
<h1>HomePage</h1>
<p>Find me in "./web/src/pages/HomePage/HomePage.js"</p>
<p>
My default route is named "home", link to me with `
<Link to="home">routes.home()</Link>`
</p>
</>
)
}
export default HomePage
Let’s clean up our component. We’ll only have a single route for now so we can delete the Link
import and routes.home()
, and we’ll delete everything except a single <h1>
tag.
// web/src/pages/HomePage/HomePage.js
const HomePage = () => {
return (
<>
<h1>RedwoodJS+Fauna</h1>
</>
)
}
export default HomePage
Cells
Cells provide a simpler and more declarative approach to data fetching. They contain the GraphQL query, loading, empty, error, and success states, each one rendering itself automatically depending on what state your cell is in.
Create a folder in web/src/components
called PostsCell
and inside that folder create a file called PostsCell.js
with the following code:
// web/src/components/PostsCell/PostsCell.js
export const QUERY = gql`
query POSTS {
posts {
data {
title
}
}
}
`
export const Loading = () => <div>Loading posts...</div>
export const Empty = () => <div>No posts yet!</div>
export const Failure = ({ error }) => <div>Error: {error.message}</div>
export const Success = ({ posts }) => {
const {data} = posts
return (
<ul>
{data.map(post => (
<li>{post.title}</li>
))}
</ul>
)
}
We’re exporting a GraphQL query that will fetch the posts in the database. We use object destructuring to access the data object and then we map over that response data to display a list of our posts. To render our list of posts we need to import PostsCell
in our HomePage.js
file and return the component.
// web/src/pages/HomePage/HomePage.js
import PostsCell from 'src/components/PostsCell'
const HomePage = () => {
return (
<>
<h1>RedwoodJS+Fauna</h1>
<PostsCell />
</>
)
}
export default HomePage
Schema Definition Language
In our graphql
directory we’ll create a file called posts.sdl.js
containing our GraphQL schema. In this file we’ll export a schema object containing our GraphQL schema definition language. It is defining a Post
type which has a title
that is the type of String
.
Fauna automatically creates a PostPage
type for pagination which has a data type that’ll contain an array with every Post
. When we create our database you will need to import this schema so Fauna knows how to respond to our GraphQL queries.
// api/src/graphql/posts.sdl.js
import gql from 'graphql-tag'
export const schema = gql`
type Post {
title: String
}
type PostPage {
data: [Post]
}
type Query {
posts: PostPage
}
`
DB
When we generated our project, db
defaulted to an instance of PrismaClient
. Since Prisma does not support Fauna at this time we will be using the graphql-request
library to query Fauna’s GraphQL API. First make sure to add the library to your project.
yarn workspace api add graphql-request graphql
To access our FaunaDB database through the GraphQL endpoint we’ll need to set a request header containing our database key.
// api/src/lib/db.js
import { GraphQLClient } from 'graphql-request'
export const request = async (query = {}) => {
const endpoint = 'https://graphql.fauna.com/graphql'
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
authorization: 'Bearer <FAUNADB_KEY>'
},
})
try {
return await graphQLClient.request(query)
} catch (error) {
console.log(error)
return error
}
}
Services
In our services directory we’ll create a posts directory with a file called posts.js
. Services are where Redwood centralizes all business logic. These can be used by your GraphQL API or any other place in your backend code. The posts function is querying the Fauna GraphQL endpoint and returning our posts data so it can be consumed by our PostsCell
.
// api/src/services/posts/posts.js
import { request } from 'src/lib/db'
import { gql } from 'graphql-request'
export const posts = async () => {
const query = gql`
{
posts {
data {
title
}
}
}
`
const data = await request(query, 'https://graphql.fauna.com/graphql')
return data['posts']
}
Let’s take one more look at our entire directory structure before moving on to the Fauna Shell.
├── api
│ └── src
│ ├── functions
│ │ └── graphql.js
│ ├── graphql
│ │ └── posts.sdl.js
│ ├── lib
│ │ └── db.js
│ └── services
│ └── posts
│ └── posts.js
└── web
├── public
│ ├── favicon.png
│ ├── README.md
│ └── robots.txt
└── src
├── components
│ └── PostsCell
│ └── PostsCell.js
├── layouts
├── pages
├── FatalErrorPage
├── HomePage
│ └── HomePage.js
└── NotFoundPage
├── index.css
├── index.html
├── index.js
└── Routes.js
Fauna Database
Create FaunaDB account
You’ll need a FaunaDB account to follow along but it’s free for creating simple low traffic databases. You can use your email to create an account or you can use your Github or Netlify account. FaunaDB Shell does not currently support GitHub or Netlify logins so using those will add a couple extra steps when we want to authenticate with the fauna-shell.
First we will install the fauna-shell which will let us easily work with our database from the terminal. You can also go to your dashboard and use Fauna’s Web Shell.
npm install -g fauna-shell
Now we’ll login to our Fauna account so we can access a database with the shell.
fauna cloud-login
You’ll be asked to verify your email and password. If you signed up for FaunaDB using your GitHub or Netlify credentials, follow these steps, then skip the Create New Database section and continue this tutorial at the beginning of the Collections section.
Create New Database
To create your database enter the fauna create-database
command and give your database a name.
fauna create-database my_db
To start the fauna shell with our new database we’ll enter the fauna shell
command followed by the name of the database.
fauna shell my_db
Import Schema
Save the following code into a file called sdl.gql and import it to your database:
type Post {
title: String
}
type Query {
posts: [Post]
}
Collections
To test out our database we’ll create a collection with the name Post. A database’s schema is defined by its collections, which are similar to tables in other databases. After entering the command fauna shell will respond with the newly created Collection
.
CreateCollection(
{ name: "Post" }
)
{
ref: Collection("Post"),
ts: 1597718505570000,
history_days: 30,
name: 'Post'
}
Create
The Create
function adds a new document to a collection. Let’s create our first blog post:
Create(
Collection("Post"),
{
data: {
title: "Deno is a secure runtime for JavaScript and TypeScript"
}
}
)
{
ref: Ref(Collection("Post"), "274160525025214989"),
ts: 1597718701303000,
data: {
title: "Deno is a secure runtime for JavaScript and TypeScript"
}
}
Map
We can create multiple blog posts with the Map
function. We are calling Map
with an array of posts and a Lambda
that takes post_title
as its only parameter. post_title
is then used inside the Lambda
to provide the title field for each new post.
Map(
[
"Vue.js is an open-source model–view–viewmodel JavaScript framework for building user interfaces and single-page applications",
"NextJS is a React framework for building production grade applications that scale"
],
Lambda("post_title",
Create(
Collection("Post"),
{
data: {
title: Var("post_title")
}
}
)
)
)
[
{
ref: Ref(Collection("Post"), "274160642247624200"),
ts: 1597718813080000,
data: {
title:
"Vue.js is an open-source model–view–viewmodel JavaScript framework for building user interfaces and single-page applications"
}
},
{
ref: Ref(Collection("Post"), "274160642247623176"),
ts: 1597718813080000,
data: {
title:
"NextJS is a React framework for building production grade applications that scale"
}
}
]
Indexes
Now we’ll create an index for retrieving all the posts in our collection.
CreateIndex({
name: "posts",
source: Collection("Post")
})
{
ref: Index("posts"),
ts: 1597719006320000,
active: true,
serialized: true,
name: "posts",
source: Collection("Post"),
partitions: 8
}
Match
Index
returns a reference to an index which Match
accepts and uses to construct a set. Paginate
takes the output from Match
and returns a Page
of results fetched from Fauna. Here we are returning an array of references.
Paginate(
Match(
Index("posts")
)
)
{
data: [
Ref(Collection("Post"), "274160525025214989"),
Ref(Collection("Post"), "274160642247623176"),
Ref(Collection("Post"), "274160642247624200")
]
}
Lambda
We can get an array of references to our posts, but what if we wanted an array of the actual data contained in the reference? We can Map
over the array just like we would in any other programming language.
Map(
Paginate(
Match(
Index("posts")
)
),
Lambda(
'postRef', Get(Var('postRef'))
)
)
{
data: [
{
ref: Ref(Collection("Post"), "274160525025214989"),
ts: 1597718701303000,
data: {
title: "Deno is a secure runtime for JavaScript and TypeScript"
}
},
{
ref: Ref(Collection("Post"), "274160642247623176"),
ts: 1597718813080000,
data: {
title:
"NextJS is a React framework for building production grade applications that scale"
}
},
{
ref: Ref(Collection("Post"), "274160642247624200"),
ts: 1597718813080000,
data: {
title:
"Vue.js is an open-source model–view–viewmodel JavaScript framework for building user interfaces and single-page applications"
}
}
]
}
So at this point we have our Redwood app set up with just a single:
- Page -
HomePage.js
- Cell -
PostsCell.js
- Function -
graphql.js
- SDL -
posts.sdl.js
- Lib -
db.js
- Service -
posts.js
We used FQL functions in the Fauna Shell to create a database and seed it with data. FQL functions included:
- CreateCollection - Create a collection
- Create - Create a document in a collection
- Map - Applies a function to all array items
- Lambda - Executes an anonymous function
- Get - Retrieves the document for the specified reference
- CreateIndex - Create an index
- Match - Returns the set of items that match search terms
- Paginate - Takes a Set or Ref, and returns a page of results
If we return to the home page we’ll see our PostsCell
is fetching the list of posts from our database.
And we can also go to our GraphiQL playground on localhost:8911/graphql
.
RedwoodJS is querying the FaunaDB GraphQL API with our posts service on the backend and fetching that data with our PostsCell on the frontend. If we wanted to extend this further we could add mutations to our schema definition language and implement full CRUD capabilities through our GraphQL client.
If you want to learn more about RedwoodJS you can check out the documentation or visit the RedwoodJS community forum. We would love to see what you’re building and we’re happy to answer any questions you have!