22
Creating a Mesh of Monolithic Microservices with StepZen and RedwoodJS
In this example we will show how you can combine multiple GraphQL APIs generated with RedwoodJS into a single StepZen schema that is then queried from another RedwoodJS app.
Because we can.
Seriously though, why would anyone ever do this? What if you wanted one app to function as a standalone CMS with a public endpoint and another for user management? You could create two separate Redwood apps, one that implements authentication and another that just holds content.
yarn create redwood-app stepzen-redwood-posts
cd stepzen-redwood-posts
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}
First you need to create a Railway account.
Install the Railway CLI
railway login
đźšť Logging in... No dice? Try railway login --browserless
đźš„ Logging in...
🎉 Logged in as Anthony Campolo ([email protected])
Run the following command and select "Create new Project."
railway init
âś” Create new Project
âś” Enter project name: stepzen-redwood-posts
âś” Environment: production
🎉 Created project stepzen-redwood-posts
Add a plugin to your Railway project.
railway add
Select PostgreSQL.
âś” Plugin: postgresql
🎉 Created plugin postgresql
Create a .env
file with your DATABASE_URL.
echo DATABASE_URL=`railway variables get DATABASE_URL` > .env
Running yarn rw prisma migrate dev
generates the folders and files necessary to create a new migration. We will name our migration posts-table
.
yarn rw prisma migrate dev --name posts-table
yarn rw g scaffold post
Start the development server and open http://localhost:8910/posts
to create a couple blog posts.
yarn rw dev
The following command will generate the configuration file needed to deploy to Netlify.
yarn rw setup deploy netlify
This generates the following netlify.toml
file:
[build]
command = "yarn rw deploy netlify"
publish = "web/dist"
functions = "api/dist/functions"
[dev]
framework = "redwoodjs"
targetPort = 8910
port = 8888
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
This lets Netlify know that:
- Your
build
command isyarn rw deploy netlify
- The
publish
directory for your assets isweb/dist
- Your
functions
will be inapi/dist/functions
After creating a GitHub repository and connecting that to your Netlify account, Netlify will build and deploy the project for you using the settings provided.
Create a blank repository at repo.new and push the project to GitHub.
git init
git add .
git commit -m "posts"
git remote add origin https://github.com/ajcwebdev/stepzen-redwood-posts.git
git push -u origin main
Go to Netlify and connect the repo.
Make sure to include your DATABASE_URL
environment variable and add ?connection_limit=1
to the end or your database will spontaneously burst into flames.
Send a query to https://stepzen-redwood-posts.netlify.app/.netlify/functions/graphql
.
query getPosts {
posts {
id
title
body
createdAt
}
}
We'll repeat most of those steps again. For simplicity of demonstrating how to stitch together multiple applications we won't actually implement auth. You can do so by following along with the authentication section of the official RedwoodJS tutorial.
yarn create redwood-app stepzen-redwood-users
cd stepzen-redwood-users
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}
model User {
id Int @id @default(autoincrement())
name String
}
railway init
âś” Create new Project
âś” Enter project name: stepzen-redwood-users
âś” Environment: production
🎉 Created project stepzen-redwood-users
Add a plugin to your Railway project.
railway add
Select PostgreSQL.
âś” Plugin: postgresql
🎉 Created plugin postgresql
Create a .env
file with your DATABASE_URL.
echo DATABASE_URL=`railway variables get DATABASE_URL` > .env
Running yarn rw prisma migrate dev
generates the folders and files necessary to create a new migration. We will name our migration users-table
.
yarn rw prisma migrate dev --name users-table
yarn rw g scaffold user
Start the development server and open http://localhost:8910/users
to create a couple users.
yarn rw dev
yarn rw setup deploy netlify
Create a blank repository at repo.new
git init
git add .
git commit -m "users"
git remote add origin https://github.com/ajcwebdev/stepzen-redwood-users.git
git push -u origin main
Go to Netlify and connect the repo.
Make sure to include your DATABASE_URL
environment variable and add ?connection_limit=1
to the end or your database will spontaneously burst into flames.
Send a query to https://stepzen-redwood-posts.netlify.app/.netlify/functions/graphql
.
query getUsers {
users {
id
name
}
}
The StepZen project will combine both Redwood apps into a single schema with the @graphql
directive.
mkdir stepzen-redwood-mesh
cd stepzen-redwood-mesh
Create an index.graphql
file.
touch index.graphql
This file tells StepZen how to assemble the various type definition files into a complete GraphQL schema.
schema
@sdl(
files: [
"schema/posts.graphql"
"schema/users.graphql"
]
) {
query: Query
}
Create a directory for your schema. The schema
directory will contain files for each GraphQL API.
mkdir schema
Create a posts.graphql
file for your Post
type.
touch schema/posts.graphql
type Post {
id: Int!
title: String!
body: String!
createdAt: DateTime!
}
type Query {
posts: [Post!]!
@graphql(
endpoint:"https://stepzen-redwood-posts.netlify.app/.netlify/functions/graphql"
)
}
Create a users.graphql
file for your User
type.
touch schema/users.graphql
type User {
id: Int!
name: String!
}
type Query {
users: [User!]!
@graphql(
endpoint:"https://stepzen-redwood-users.netlify.app/.netlify/functions/graphql"
)
}
stepzen start
You will be asked to name your endpoint. I will call mine api/stepzen-redwood-mesh
.
Enter the following query, hold your breath, cross your fingers, say seventeen Hail Marys, and run the query.
query MySuperAwesomeQueryThatWontFail {
users {
name
id
}
posts {
title
id
createdAt
body
}
}
Now that we've created our API, we need to connect another Redwood app to StepZen to get the data into our web side.
yarn create redwood-app stepzen-metawood
cd stepzen-metawood
The api/src
directory contains all the other backend code for a Redwood app and includes four directories:
functions
graphql
lib
services
The functions
directory contains a graphql.js
file auto-generated by Redwood that is required to use the GraphQL API. Since we will not use the Prisma client or a database that Redwood comes preconfigured for, we can replace the default template with the following code.
// api/src/functions/graphql.js
import {
createGraphQLHandler,
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
export const handler = createGraphQLHandler({
schema: makeMergedSchema({
schemas,
services: makeServices({ services }),
}),
})
The graphql
directory contains posts.sdl.js
with your GraphQL schema written in the Schema Definition Language. This will ensure that our Redwood API will have a schema
that matches our schema
in posts.graphql
.
touch api/src/graphql/posts.sdl.js
The schema includes a Post
type, and each Post
has an id
, title
, body
, and createdAt
date just like our StepZen schema. The posts
query returns an array of Post
objects.
// api/src/graphql/posts.sdl.js
export const schema = gql`
type Post {
id: ID
title: String
body: String
createdAt: String
}
type Query {
posts: [Post]
}
`
Create a users.sdl.js
file so our Redwood API will have a schema
that matches our schema
in users.graphql
.
touch api/src/graphql/users.sdl.js
The schema includes a User
type, and each User
has an id
and name
just like our StepZen schema. The users
query returns an array of User
objects.
// api/src/graphql/users.sdl.js
export const schema = gql`
type User {
id: ID
name: String
}
type Query {
users: [User]
}
`
While Redwood's web
side includes Apollo Client by default, its api
side does not include any built in mechanism for making HTTP requests.
We will follow the model of numerous community projects that have used graphql-request
to connect to services such as Contentful, AppSync, Hasura, and FaunaDB. First, we need to install graphql-request
as a dependency on the api
side.
yarn workspace api add graphql-request
Since we will not be using the Prisma Client we can rename db.js
to client.js
mv api/src/lib/db.js api/src/lib/client.js
Include the following code in the newly named file.
// api/src/lib/client.js
import { GraphQLClient } from 'graphql-request'
export const request = async (query = {}) => {
const endpoint = process.env.API_ENDPOINT
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
authorization: 'apikey ' + process.env.API_KEY
},
})
try {
return await graphQLClient.request(query)
} catch (err) {
console.log(err)
return err
}
}
This code uses graphql-request
to connect to StepZen and query the API along with our StepZen API key in the header for authorization.
-
endpoint
is set to the url generated when we deployed our API withstepzen start
. -
authorization
includes your StepZen API key appended toapikey
. You can get your API key on your my account page.
Let's create the .env
file that will contain our StepZen API key and endpoint URL.
API_ENDPOINT=<YOUR_API_ENDPOINT>
API_KEY=<YOUR_API_KEY>
In the services
directory we will create a posts
directory with a posts.js
service and a users
directory with a users.js
service. These files will send GraphQL queries to our StepZen API.
mkdir api/src/services/posts api/src/services/users
touch api/src/services/posts/posts.js api/src/services/users/users.js
We will include code for querying data with GraphQL.
// api/src/services/posts/posts.js
import { request } from 'src/lib/client'
import { gql } from 'graphql-request'
export const posts = async () => {
const GET_POSTS_QUERY = gql`
query getPosts {
posts {
id
title
body
createdAt
}
}
`
const data = await request(GET_POSTS_QUERY)
return data['posts']
}
GET_POSTS_QUERY
is sent with the GraphQLClient
imported from src/lib/client
. The query is asking for the list of posts
and their id
, title
, body
, and createdAt
date.
// api/src/services/users/users.js
import { request } from 'src/lib/client'
import { gql } from 'graphql-request'
export const users = async () => {
const GET_USERS_QUERY = gql`
query getUsers {
users {
id
name
}
}
`
const data = await request(GET_USERS_QUERY)
return data['users']
}
GET_USERS_QUERY
is sent with the GraphQLClient
imported from src/lib/client
. The query is asking for the list of users
and their id
and name
.
The api
side can be accessed through a GraphiQL explorer running on localhost:8911/graphql
.
Now that the API and query are set up, we need to connect the web interface to display the returned data. The web
side contains a PostsCell
for fetching posts
, a UsersCell
for fetching users
, and a HomePage
for rendering the cell.
Create a PostsCell
.
yarn rw g cell posts
getPosts
returns the id
, title
, body
, and createdAt
date of each post
. This will send the query to our api
side, which in turn sends a query to our StepZen API which in turn sends a query to our stepzen-redwood-posts
API. Once the results are returned, they will be output on the page. Redwood automatically adds basic handling for the Loading
, Empty
and Failure
states.
// web/src/components/PostsCell/PostsCell.js
export const QUERY = gql`
query getPosts {
posts {
id
title
body
createdAt
}
}
`
export const Loading = () => <div>Almost there...</div>
export const Empty = () => <div>WE NEED POSTS</div>
export const Failure = ({ error }) => <div>{error.message}</div>
export const Success = ({ posts }) => {
return (
<ul>
{posts.map(post => (
<li>{post.title}</li>
))}
</ul>
)
}
Create a UsersCell
.
yarn rw g cell users
getUsers
returns the id
and name
of each user
. This will send the query to our api
side, which in turn sends a query to our StepZen API which in turn sends a query to our stepzen-redwood-users
API.
// web/src/components/UsersCell/UsersCell.js
export const QUERY = gql`
query getUsers {
users {
id
name
}
}
`
export const Loading = () => <div>Almost there...</div>
export const Empty = () => <div>WE NEED USERS</div>
export const Failure = ({ error }) => <div>{error.message}</div>
export const Success = ({ users }) => {
return (
<ul>
{users.map(user => (
<li>{user.name}</li>
))}
</ul>
)
}
Finally, let's create the home page.
yarn rw g page home /
All we need to do in this file is import PostsCell
and UsersCell
to display the information fetched by the respective cell's queries.
// web/src/pages/HomePage/HomePage.js
import PostsCell from 'src/components/PostsCell'
import UsersCell from 'src/components/UsersCell'
const HomePage = () => {
return (
<>
<h1>StepZen+Metawood</h1>
<h2>Posts</h2>
<PostsCell />
<h2>Users</h2>
<UsersCell />
</>
)
}
export default HomePage
22