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.

Uhhhh... sounds cool, but why?

Because we can.

Create Redwood Apps

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.

Create Redwood App for Posts

yarn create redwood-app stepzen-redwood-posts
cd stepzen-redwood-posts

Create Posts schema

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())
}

Provision a PostgreSQL database with Railway

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

Initialize project

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

Provision PostgreSQL

Add a plugin to your Railway project.

railway add

Select PostgreSQL.

βœ” Plugin: postgresql 
πŸŽ‰ Created plugin postgresql

Set environment variable

Create a .env file with your DATABASE_URL.

echo DATABASE_URL=`railway variables get DATABASE_URL` > .env

Setup database with Prisma Migrate

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

Generate scaffold

yarn rw g scaffold post

Start the development server and open http://localhost:8910/posts to create a couple blog posts.

yarn rw dev

Setup Netlify Deploy

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 is yarn rw deploy netlify
  • The publish directory for your assets is web/dist
  • Your functions will be in api/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.

Push Project to GitHub

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

Connect Repo to Netlify

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.

Create Custom Domain Name

Test it with a query

Send a query to https://stepzen-redwood-posts.netlify.app/.netlify/functions/graphql.

query getPosts {
  posts {
    id
    title
    body
    createdAt
  }
}

Create Redwood App for Users

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

Create Users schema

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
}

Provision a PostgreSQL database with Railway

railway init
βœ” Create new Project
βœ” Enter project name: stepzen-redwood-users
βœ” Environment: production
πŸŽ‰ Created project stepzen-redwood-users

Provision PostgreSQL

Add a plugin to your Railway project.

railway add

Select PostgreSQL.

βœ” Plugin: postgresql 
πŸŽ‰ Created plugin postgresql

Set environment variable

Create a .env file with your DATABASE_URL.

echo DATABASE_URL=`railway variables get DATABASE_URL` > .env

Setup database with Prisma Migrate

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

Generate scaffold

yarn rw g scaffold user

Start the development server and open http://localhost:8910/users to create a couple users.

yarn rw dev

Setup Netlify Deploy

yarn rw setup deploy netlify

Push Project to GitHub

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

Connect Repo to Netlify

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.

Create Custom Domain Name

Test it with a query

Send a query to https://stepzen-redwood-posts.netlify.app/.netlify/functions/graphql.

query getUsers {
  users {
    id
    name
  }
}

Create StepZen Project

The StepZen project will combine both Redwood apps into a single schema with the @graphql directive.

mkdir stepzen-redwood-mesh
cd stepzen-redwood-mesh

index.graphql

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
}

posts.graphql

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"
    )
}

users.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"
    )
}

Deploy your endpoint with stepzen start

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

Create Meta-Redwood App

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

Oh god what have I done

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 }),
  }),
})

posts.sdl.js

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

users.sdl.js

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

client.js

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 with stepzen start.
  • authorization includes your StepZen API key appended to apikey. You can get your API key on your my account page.

Create .env file

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>

posts.js

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.

The Redwood Web Side

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.

PostsCell

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

UsersCell

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

HomePage

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

View your new monstrosity

And there you go, now you can tell your boss you know how to create a service mesh made of monolithic microservices and you need a 20% raise immediately. The code for this project can be found on my GitHub.

22