ajcwebdev

building a minimum viable stack with redwoodJS and faunaDB

redwoodjsreactfaunadbgraphql

Introduction

RedwoodJS

RedwoodJS is an opinionated, full-stack, serverless web application framework for building and deploying Jamstack applications. It was created by Tom-Preston Werner and combines a variety of technologies and techniques that have been influencing the industry over the last 5-10 years. This includes:

My dream of a future is for something I call a universal deployment machine, which means I write my code. It's all text, I just write text. Then I commit to GitHub. Then it's picked up and it's deployed into reality. That's it, that's the whole thing.

Tom Preston Warner
RedwoodJS Shoptalk (May 11, 2020)

FaunaDB

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.

01-redwood-starter-page

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
│   ├── 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 /

02-generated-HomePage

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

03-new-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

04-PostsCell-no-posts

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]
}

Alt Text

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:

We used FQL functions in the Fauna Shell to create a database and seed it with data. FQL functions included:

If we return to the home page we'll see our PostsCell is fetching the list of posts from our database.

Alt Text

And we can also go to our GraphiQL playground on localhost:8911/graphql.

Alt Text

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!