Build the RedwoodJS Example Todo App with Airtable as Backend
The RedwoodJS Example Todo is one of two canonical RedwoodJS applications, the another being the RedwoodJS Example Blog. Both of these applications use Prisma Client to connect to a PostgreSQL database.
However, we can easily swap out Prisma and connect to another database such as Neo4j or Fauna via a GraphQL endpoint.
This article describes another option - how to use StepZen to swap in Airtable for the database in the Example Todo app.
Setup
Clone Example Todo repo
git clone https://github.com/redwoodjs/example-todo.git
Install dependencies and start development server
cd example-todo
yarn
yarn rw prisma migrate dev
yarn rw dev
This will open a browser to localhost:8910
.
RedwoodJS Architecture
Beginning Project Structure
A Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo. The frontend project is called web
and the backend project is called api
. These are referred to as "sides", i.e. the "web side" and the "api side".
├── api
│ ├── src
│ │ ├── functions
│ │ │ └── graphql.js
│ │ ├── graphql
│ │ │ └── todos.sdl.js
│ │ ├── lib
│ │ │ └── db.js
│ │ └── services
│ │ └── todos
│ │ └── todos.js
│ └── db
│ ├── migrations
│ └── schema.prisma
└── web
├── public
│ └── favicon.png
└── src
├── components
│ ├── AddTodo
│ │ └── AddTodo.js
│ ├── AddTodoControl
│ │ └── AddTodoControl.js
│ ├── Check
│ │ └── Check.js
│ ├── TodoItem
│ │ └── TodoItem.js
│ └── TodoListCell
│ └── TodoListCell.js
├── pages
│ ├── FatalErrorPage
│ │ └── FatalErrorPage.js
│ ├── HomePage
│ │ └── HomePage.js
│ └── NotFoundPage
│ └── NotFoundPage.js
├── App.js
├── Routes.js
├── index.css
└── index.html
They are separate projects because code on the web
side will end up running in the user's browser while code on the api
side will run on a server somewhere.
- The
api
side is an implementation of a GraphQL API. Your business logic is organized into "services" that represent their own internal API and can be called both from external GraphQL requests and other internal services. - The
web
side is built with React and includes a router that maps URL paths to React "Page" components. Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data.
Redwood API Side
Change id
type in todos.sdl.js
Our schema
is contained in the todos.sdl.js
file. We only have to make a slight change to the schema definition language.
// api/src/graphql/todos.sdl.js
export const schema = gql`
type Todo {
id: Int!
body: String!
status: String!
}
type Query {
todos: [Todo]
}
type Mutation {
createTodo(body: String!): Todo
updateTodoStatus(id: Int!, status: String!): Todo
renameTodo(id: Int!, body: String!): Todo
}
`
For each of the three instances of id
, replace Int
with ID
.
// api/src/graphql/todos.sdl.js
export const schema = gql`
type Todo {
id: ID!
body: String!
status: String!
}
type Query {
todos: [Todo]
}
type Mutation {
createTodo(body: String!): Todo
updateTodoStatus(id: ID!, status: String!): Todo
renameTodo(id: ID!, body: String!): Todo
}
`
Replace PrismaClient
with node-fetch
If we look at our db.js
file in api/src/lib
we will see that we are importing PrismaClient
from @prisma/client
and initializing a db
variable with PrismaClient()
.
// api/src/lib/db.js
import { PrismaClient } from '@prisma/client'
export const db = new PrismaClient()
Instead of using PrismaClient, we will install node-fetch
so we can make fetch
requests from the Redwood API as described in the Using a Third Party API cookbook.
yarn workspace api add node-fetch
We no longer need our db
folder containing our Prisma migrations.
rm -rf api/db
We'll also rename the file to stepzen.js
:
mv api/src/lib/db.js api/src/lib/stepzen.js
Delete all the code in the newly named stepzen.js
file and replace it with the following:
// api/src/lib/stepzen.js
const fetch = require('node-fetch')
async function client(data) {
const response = await fetch(process.env.API_ENDPOINT, {
method: 'POST',
body: data,
headers: {
'Content-Type': 'application/json',
Authorization: 'Apikey ' + process.env.API_KEY,
},
})
const r = await response.json()
return r
}
async function todos() {
const TODOS = JSON.stringify({
query: `query Todos {
todos {
body
id
status
}
}`,
})
const res = await client(TODOS)
return res.data.todos
}
async function createTodo(input) {
const CREATE_TODO = JSON.stringify({
query: `mutation CreateTodo {
createTodo(body: "${input.data.body}") {
id
body
status
}
}`,
})
const res = await client(CREATE_TODO)
return res.data.createTodo
}
async function updateTodo(input) {
if (input.data.status !== undefined) {
const UPDATE_TODO_STATUS = JSON.stringify({
query: `mutation UpdateTodo {
updateTodoStatus(id: "${input.where.id}", status: "${input.data.status}") {
body
id
status
}
}`,
})
const res = await client(UPDATE_TODO_STATUS)
return res.data.updateTodoStatus
}
if (input.data.body !== undefined) {
const RENAME_TODO = JSON.stringify({
query: `mutation RenameTodo {
renameTodo(body: "${input.data.body}", id: "${input.where.id}") {
body
id
status
}
}`,
})
const res = await client(RENAME_TODO)
return res.data.renameTodo
}
}
Lastly, we export an sz
object with the functions we need to import into our services: findMany
, create
, and update
.
// api/src/lib/stepzen.js
export const sz = {
todo: {
findMany: () => {
return getTodos()
},
create: (input) => {
return createTodo(input)
},
update: (input) => {
return updateTodo(input)
},
},
}
Replace db
with sz
in todos.js
service
Redwood automatically connects your internal services with your frontend GraphQL client, reducing the amount of boilerplate you have to write. Your services can interact with a database via Prisma Client, or they can be written for any other backend database technology.
Redwood automatically imports and maps resolvers from the corresponding services file onto your SDL. These files are contained in the api/src/services
directory. This lets you write those resolvers in a way that makes them easy to call as regular functions from other resolvers or services.
If we look at the current todos.js
file for the Example Todo app we see that it imports db
, which was initialized with the Prisma Client.
// api/src/services/todos/todos.js
import { db } from 'src/lib/db'
export const todos = () => db.todo.findMany()
export const createTodo = ({ body }) => db.todo.create({ data: { body } })
export const updateTodoStatus = ({ id, status }) =>
db.todo.update({
data: { status },
where: { id },
})
export const renameTodo = ({ id, body }) =>
db.todo.update({
data: { body },
where: { id },
})
We simply replace db
with the sz
object we created earlier.
// api/src/services/todos/todos.js
import { sz } from 'src/lib/stepzen'
export const todos = () => sz.todo.findMany()
export const createTodo = ({ body }) => sz.todo.create({ data: { body } })
export const updateTodoStatus = ({ id, status }) =>
sz.todo.update({
data: { status },
where: { id },
})
export const renameTodo = ({ id, body }) =>
sz.todo.update({
data: { body },
where: { id },
})
Redwood Web Side
Change id
type in TodoListCell.js
On the web
side we have to make the same change in TodoListCell.js
.
We make the same change to our types that we made to our schema
on the api
side by changing Int
to ID
for the id
type.
// web/src/components/TodoListCell/TodoListCell.js
const UPDATE_TODO_STATUS = gql`
mutation TodoListCell_CheckTodo($id: ID!, $status: String!) {
updateTodoStatus(id: $id, status: $status) {
id
__typename
status
}
}
`
That's all it takes to completely rewire your Redwood project! But how is it connecting to Airtable? That's the magic of StepZen. We have a schema that allows us to directly query our REST endpoints from Airtable.
StepZen Side
A StepZen project contains the following files:
index.graphql
tells StepZen how to assemble the various type definition files into a complete GraphQL schema.- One or more GraphQL Schema Definition Language (SDL) files ending in
.graphql
. config.yaml
contains the keys and other credential information that StepZen needs to access your backend data sources.
To setup our StepZen API, let's create a stepzen
directory within our Redwood project's API folder. Within the stepzen
folder, create a schema
directory.
mkdir api/stepzen api/stepzen/schema
Every StepZen project requires an index.graphql
that ties together all of our schemas. Create an index.graphql
file for our schema
and a todos.graphql
file for our Todo
type, Query
type, and Mutation
type.
touch api/stepzen/schema/todos.graphql api/stepzen/index.graphql
todos.graphql
Let's create a Todo
GraphQL type that represents a todo being returned by the Airtable API. todos.graphql
has a Todo
type and a todos
query that returns an array of Todo
objects. Our Todo
type has only a few properties.
# api/stepzen/schema/todos.graphql
type Todo {
id: ID!
body: String!
status: String!
}
Our todos
query is connected to the Airtable REST API using StepZen's custom @rest
directive. The @rest
directive accepts the URL of the REST endpoint
that we want to connect.
# api/stepzen/schema/todos.graphql
type Query {
todos: [Todo]
@rest(
resultroot: "records[]"
setters: [
{ field: "body", path: "fields.Todo" }
{ field: "status", path: "fields.Status" }
]
endpoint: "https://api.airtable.com/v0/$baseid/todos/"
configuration: "myAirtable"
)
}
We will also need mutations for createTodo
, updateTodoStatus
, and renameTodo
.
# api/stepzen/schema/todos.graphql
type Mutation {
createTodo(body: String!, status: String! = "off"): Todo
@rest(
resultroot: "records[]"
setters: [
{ field: "body", path: "fields.Todo" }
{ field: "status", path: "fields.Status" }
]
endpoint: "https://mutate-airtable-zlwadjbovq-uc.a.run.app/POST/$baseid/todos?Todo=$body;&Status=$status;"
configuration: "myAirtable"
)
updateTodoStatus(id: ID!, status: String!): Todo
@rest(
resultroot: "records[]"
setters: [
{ field: "body", path: "fields.Todo" }
{ field: "status", path: "fields.Status" }
]
endpoint: "https://mutate-airtable-zlwadjbovq-uc.a.run.app/PATCH/$baseid/todos?Status=$status;&id=$id;"
configuration: "myAirtable"
)
renameTodo(id: ID!, body: String!): Todo
@rest(
resultroot: "records[]"
setters: [
{ field: "body", path: "fields.Todo" }
{ field: "status", path: "fields.Status" }
]
endpoint: "https://mutate-airtable-zlwadjbovq-uc.a.run.app/PATCH/$baseid/todos?Todo=$body;&id=$id;"
configuration: "myAirtable"
)
}
index.graphql
Our schema
in index.graphql
ties together all of our other schema files. For this example, we have only one - the todos.graphql
file, which is included in our @sdl
directive.
# api/stepzen/index.graphql
schema @sdl(files: ["schema/todos.graphql"]) {
query: Query
}
The index.graphql
tells StepZen how to assemble the various type definition files into a complete GraphQL schema. The @sdl
directive is a StepZen directive that specifies the list of files to assemble. It includes a comma-separated list of .graphql
files in your project folder.
config.yaml
The config.yaml
contains various configurations that can include the keys and other credential information that StepZen needs to access your backend data sources.
touch api/stepzen/config.yaml
WARNING! This file should be added to .gitignore
as it likely contains secret information.
.idea
.DS_Store
.env
.netlify
.redwood
dev.db
dist
dist-babel
node_modules
yarn-error.log
web/public/mockServiceWorker.js
config.yaml
To connect an Airtable account, we need to supply our baseid
, tablename
, and authorization
header.
configurationset:
- configuration:
name: myAirtable
baseid: <YOUR_BASEID_TOKEN>
tablename: todos
authorization: Bearer <YOUR_BEARER_TOKEN>
You can get this information in your Airtable account.
Deploy Our StepZen Endpoint
Now that our schema has been created, we can use the StepZen CLI to deploy it. If you have not already installed and configured the StepZen CLI, you can follow the instructions here.
stepzen start
This command uploads and deploys your endpoint automatically. You are asked a few questions to configure your endpoint.
1. Name your endpoint
First, you are prompted to name your endpoint destination.
? What would you like your endpoint to be called? api/todos
2. Specify root directory
The CLI detects your stepzen
directory.
? We have detected a schema in this directory. Set the schema root to "api/stepzen"? (Y/n) Y
This creates a file called stepzen.config.json
.
{
"endpoint": "api/todos",
"root": "api/stepzen"
}
This also deploys the schema to StepZen and lets you explore the endpoint from the StepZen dashboard. It also watches the directory for changes so that any changes that you make to your schema code is automatically uploaded and redeployed.
3. Create .env
file and restart the development server
Create the .env
file in your Redwood project to hold your StepZen API key and endpoint URL.
touch .env
API_ENDPOINT=<YOUR_API_ENDPOINT>
API_KEY=<YOUR_API_KEY>
Restart your development server so Redwood can inject the environment variables and return to localhost:8910
.
Final Project Structure
├── api
│ ├── src
│ │ ├── functions
│ │ │ └── graphql.js
│ │ ├── graphql
│ │ │ └── todos.sdl.js
│ │ ├── lib
│ │ │ └── stepzen.js
│ │ └── services
│ │ └── todos
│ │ └── todos.js
│ └── stepzen
│ ├── config.yaml
│ ├── index.graphql
│ ├── stepzen.config.json
│ └── todos
│ └── todos.graphql
└── web
├── public
│ └── favicon.png
└── src
├── components
│ ├── AddTodo
│ │ └── AddTodo.js
│ ├── AddTodoControl
│ │ └── AddTodoControl.js
│ ├── Check
│ │ └── Check.js
│ ├── TodoItem
│ │ └── TodoItem.js
│ └── TodoListCell
│ └── TodoListCell.js
├── pages
│ ├── FatalErrorPage
│ │ └── FatalErrorPage.js
│ ├── HomePage
│ │ └── HomePage.js
│ └── NotFoundPage
│ └── NotFoundPage.js
├── App.js
├── Routes.js
├── index.css
└── index.html
Where to go from here
To learn more about StepZen and how to build a schema, visit the StepZen docs. To learn more about how to configure Redwood and build the frontend, visit the Redwood docs.
You can find more example repositories at the StepZen Github. If you're not already signed up for StepZen, you can sign up here.