Editor's Note: This blog describes how Isaac uses GraphQL and StepZen to build an event organizing application. Using StepZen he converts REST API endpoints to GraphQL in minutes and links types with the @materializer directive. When it's ready, we look forward to linking and sharing Isaac's detailed walk-through of the app in this article. For now, follow Isaac on Twitter for updates, https://twitter.com/isaacwgarcia.

Organizing Events and Attendees with StepZen's @materializer

In this tutorial we build a dashboard that pulls in all event and attendees associated with an event organizer. You can see the event app here: https://eventdemo.vercel.app/

There are also dummy REST endpoints to help you build your own StepZen endpoint: https://eventdemo.vercel.app/api/owner?email=isaac@gmail.com

Firestore Data Model (optional)

Event owners need a reference point to organize all the events and attendees. I chose Firebase to store the event owner, event, and attendee information for the dashboard.

For a quickstart to create a Firestore Database, see Get started with Cloud Firestore.

Cloud Firestore is schemaless, so you have complete freedom over what fields you put in each document and what data types you store in those fields. Documents in the same collection can all contain different fields with different types for each field. However, it's a good idea to use the same fields and data types across multiple documents so that you can query the documents more easily.

The application will be linking firestore data for an event owner to review and coordinate upcoming, live, and past events. To organize this, we create three firestore collections.

Firestore Data Model

  1. Events: The event that is going to occur or has previously occured.
└── Event (collection)
    └── {{ unique_event_id }} (document)
        ├── attendee_ids (array[strings])
        ├── id (string)
        ├── location (string)
        └── name (string)
  1. Attendees: This is not a list of those that are going to attend an event, but those that have attended a past or on-going event.
└── Attendee (collection)
    └── {{ unique_attendee_id }} (document)
        ├── event_ids (array[strings])
        ├── address (string)
        ├── email (string)
        ├── id (string)
        └── name (string)
  1. Event Owners: The event organizer that is associated with events by id.
└── Owner
    └── {{ unique_owner_id }}
        ├── event_ids (array[strings])
        ├── email (string)
        ├── id (string)
        └── name (string)

Build your NextJS App

First, run create-next-app.

npx create-next-app name_of_the_app

Install additional dependencies.

npm install next-connect typescript @types/node @types/react react-dom firebase

Building the Firebase REST Endpoints (optional)

Why REST endpoints to Firebase? Firebase provides a really well documented npm package for JavaScript applications. These REST endpoints will pair nicely with the GraphQL endpoint we build later on.

Open the app you just created to begin building the api REST endpoints. Building an API in NextJS is simple with the API Routes functionality. Just by creating an api folder in pages directory, the endpoint /api/ is ready to be curated.

By default, create-next-app generates an /api/hello.js file.

cd pages && cd api

First we need to initialize the Firestore Database Client. Defining our environment variables:

// next.config.js
module.exports = {
  reactStrictMode: true,
  env: {
    STEPZEN_API_KEY: process.env.STEPZEN_API_KEY,
    STEPZEN_API_URL: process.env.STEPZEN_API_URL,
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
  },
}

These firebase configuration values are added to the firebaseConfig object that we will create in our components folder:

mkdir components
cd components
// components/firebaseConfig.ts
const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
}
export default firebaseConfig

In root, we define a file .env.local within the values:

### .env.local
STEPZEN_API_KEY=
STEPZEN_API_URL=
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=

Create a lib folder that will contain db.ts. The firebaseConfig will initialize the firebase app here:

// components/lib/db.ts
//Firebase v9.6.6

import firebaseConfig from '../firebaseConfig'
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)

export { db }

And now to write all the promises for the REST endpoint, create a dbUtil.ts.

Copy the full file here in the eventdemo repo.

// components/lib/dbUtil.ts
import { Event, Owner, Attendee } from '../lib/types'
import {
  collection,
  doc,
  getDoc,
  setDoc,
  query,
  where,
  getDocs,
  QuerySnapshot,
} from 'firebase/firestore'
import { db } from './db'

export async function createEvent(event: Event): Promise<void> {
  const eventRef = collection(db, 'Event')
  await setDoc(doc(eventRef, event.id), {
    attendee_ids: event.attendee_ids,
    date: event.date,
    description: event.description,
    location: event.location,
    name: event.name,
  })
}

export async function readEvent(id: string): Promise<Event> {
  const event = await getDoc(doc(db, 'Event', id))

  if (event.exists()) {
    return event.data() as Event
  } else {
    const json = '{"status":"not found"}'
    return JSON.parse(json)
  }
}

[..]

Write the types to ensure that specific objects are being handled in the typescript file for dbUtil.ts

// components/lib/types.ts
export interface Attendee {
  id: string
  name: string
  address: string
  phone: string
  email: string
  event_ids: String[]
}

export interface Event {
  id: string
  name: string
  date: number
  location: string
  description: string
  attendee_ids: String[]
}

export interface Owner {
  id: string
  name: string
  event_ids: String[]
  email: string
  phone: string
  username: string
  about: string
}

To save time, I recommend pulling down the code from the eventdemo repo.

└── pages
    └── api
        ├── event
        ├── eventList
        ├── attendee
        ├── attendeeList
        ├── owner
        ├── ownerList

Run the application

npm run dev

The endpoint http://localhost:3000/api/owner?email=isaac@gmail.com should return an owner object.

{
  "email": "isaac@gmail.com",
  "name": "Isaac Garcia",
  "id": "cSnwweJEE1PiTEinPbdTc",
  "event_ids": ["r4362w342pen"]
}

Create the StepZen GraphQL Endpoint

At this point, the app is serving REST endpoints. Now it is time to retrieve the data from the /api/ REST endpoints and unify our data with StepZen.

If you want to use your own endpoints rather than the endpoints at https://eventdemo.vercel.app/api, we'll need to deploy the /api/ REST endpoints to a public URL. To do so, we can deploy our NextJS app with Vercel. Copy the code here and deploy the repo to a new or existing vercel account, https://vercel.com/new.

Create a new project in Vercel: Vercel > Import the Git Repository. Remember to add the environment variables from the .env.local file to the project's configuration and deploy. The REST endpoints are set up and ready to be connected in the StepZen schema.

Creating the GraphQL Schema

In the StepZen folder, we create a schema (SDL) file named eventdemo.graphql. It matches the Firestore collections of owners, events, and attendees and also has the REST endpoints, which for this example we will use:

https://eventdemo.vercel.app/

In stepzen/eventdemo.graphql:

type Owner {
  id: String
  name: String
  email: String
  event_ids: [String]
  """
  Events that a person has been to.
  """
  events: [Event]
    @materializer(
      query: "event_list_by_id"
      arguments: [{ name: "id", field: "event_ids" }]
    )
}

type Owner_Events {
  id: String
  name: String
  email: String
  event_ids: [String]
}

type Event {
  form_fields: [Form_field]
  id: String
  location: String
  name: String
  attendee_ids: [String]!
  """
  People that have attended an event.
  """
  attendees: [Attendee]
    @materializer(
      query: "person_list_by_id"
      arguments: [{ name: "id", field: "attendee_ids" }]
    )
}

type Attendee {
  address: String
  email: String
  event_ids: [String]
  id: String
  name: String
  phone: String
  """
  Events that a person has been to.
  """
  events: [Event]
    @materializer(
      query: "event_list_by_id"
      arguments: [{ name: "id", field: "event_ids" }]
    )
}

type Query {
  """
  Equivalent To GET /api/events/id
  """
  firebase_event_by_id(id: String!): Event
    @rest(endpoint: "https://eventdemo.vercel.app/api/event/$id")

  """
  Equivalent To GET /api/person?email=$email
  """
  owner_by_email(email: String!): Owner
    @rest(endpoint: "https://eventdemo.vercel.app/api/owner?email=$email")
  """
  Equivalent To GET /api/eventList?id=$id
  """
  event_list_by_id(id: [String]): [Event]
    @rest(endpoint: "https://eventdemo.vercel.app/api/eventList?")
  """
  Equivalent To GET /api/personList?id=$id
  """
  person_list_by_id(id: [String]!): [Attendee]
    @rest(endpoint: "https://eventdemo.vercel.app/api/attendeeList?")

  """
  Equivalent To GET /api/owner/id
  """
  rest_owner_events_by_email(email: String!): Owner_Events
    @rest(endpoint: "https://eventdemo.vercel.app/api/owner?email=$email")
}

As shown in the schema above, there are two distinct fields in each type that links owners, events, and attendees into a single query.

For Owner, the event_ids return an array of strings, which can be passed as an argument to the events @materializer. StepZen appends this array of ids to the @rest endpoint.

type Owner {
  id: String
  name: String
  email: String
  event_ids: [String]
  """
  Events that a person has been to.
  """
  events: [Event]
    @materializer(
      query: "event_list_by_id"
      arguments: [{ name: "id", field: "event_ids" }]
    )
}

The same linking process is done in the Event type to retrieve all attendees associated with an individual event.

type Event {
  form_fields: [Form_field]
  id: String
  location: String
  name: String
  attendee_ids: [String]!
  """
  People that have attended an event.
  """
  attendees: [Attendee]
    @materializer(
      query: "person_list_by_id"
      arguments: [{ name: "id", field: "attendee_ids" }]
    )
}

Deploying and running your GraphQL endpoint

In the StepZen folder, run stepzen start and your GraphQL endpoint is generated. It is deployed and live immediately on StepZen.

Querying your GraphQL endpoint running on StepZen

Returning to the root of the project, adding the STEPZEN_API_URL and getting your STEPZEN_API_KEY from your My StepZen account page now provides a GraphQL API to the dashboard.

### .env.local

STEPZEN_API_KEY=
STEPZEN_API_URL=
FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
FIREBASE_APP_ID=

Expose the environment variables to the application.

// next.config.js

module.exports = {
  reactStrictMode: true,
  env: {
    STEPZEN_API_KEY: process.env.STEPZEN_API_KEY,
    STEPZEN_API_URL: process.env.STEPZEN_API_URL,
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
  },
}

To test the endpoint in our local enviroment, under our StepZen folder, we simply run stepzen start and navigate to the GraphiQL browser:

http://localhost:5001/api/eventdemoapp

Integrating the Frontend

Let's install a popular UI React Framework, MaterialUI.

npm install @mui/material @emotion/react @emotion/styled

To call the StepZen endpoint, we create a fetchAPI() function with our GraphQL query.

// components/lib/api.ts
import { APIConnection } from '../../stepzen/stepzenTypes'

async function fetchAPI(query: any, { variables }: APIConnection = {}) {
  const headers = {
    Authorization: `Apikey ${process.env.STEPZEN_API_KEY}`,
    'Content-Type': 'application/json',
  }

  const res = await fetch(`${process.env.STEPZEN_API_URL}`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      query,
      variables,
    }),
  })

  const json = await res.json()
  if (json.errors) {
    console.error(json.errors)
    throw new Error('Failed to fetch API')
  }
  return json.data
}

export async function getOwner(email: any) {
  try {
    const data = await fetchAPI(
      `
      query MyQuery {
        owner_by_email(email: "${email}") {
          name
          events {
            name
            location
            attendees {
              name
              email
              phone
            }
          }
        }
      }
      `
    )
    return data?.owner_by_email
  } catch (e) {
    return e.message
  }
}

In the same file, the owner_by_email() function passes our query owner_by_email to fetchAPI().

export async function getOwner(email: any) {
  try {
    const data = await fetchAPI(
      `
      query MyQuery {
        owner_by_email(email: "${email}") {
          name
          events {
            name
            location
            attendees {
              name
              email
              phone
            }
          }
        }
      }
      `
    )
    return data?.owner_by_email
  } catch (e) {
    return e.message
  }
}

If the event owner email exists in our database, StepZen responds with the owner and all the associated events, which are followed by a nested array of attendees to each event, just like magic.

Now we can create a personalized page for each owner, using dynamic routes.

Create a personalized page for each owner

Build out the owner page to view the events and attendees associated with isaac@gmail.com.

Copy the full file here in the eventdemo repo.

// pages/owner/[id].tsx

import * as React from 'react'
import { styled } from '@mui/material/styles'
import Grid from '@mui/material/Grid'
import Paper from '@mui/material/Paper'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import ButtonBase from '@mui/material/ButtonBase'
import { getOwner } from '../../components/lib/api'

const Img = styled('img')({
  margin: 'auto',
  display: 'block',
  maxWidth: '100%',
  maxHeight: '100%',
})
function Owner(data: any) {
  return (
    [...]
  )
}

export async function getStaticPaths() {
  return {
    paths: [{ params: { id: 'isaac@gmail.com' } }],
    // Enable statically generating additional pages

    fallback: true, //if it didnt render, it will render once requested.
  }
}

export const getStaticProps = async (context: any) => {
  const email = context.params.id
  const response = await getOwner(email)
  if (response.events)
    return {
      props: {
        email: email,
        events: response.events,
      },
    }
  else {
    return {
      props: {},
    }
  }
}

export default Owner

Run the application

npm run dev

And visit the /owner/isaac@gmail.com page http://localhost:3000/owner/isaac@gmail.com

Voila! A page per owner is now showing the events and its attendees.

Where to Go From Here

The example above highlights the ease of linking data together to provide value to an event owner. For a production ready dashboard, a login experience is required. Here we focused on the backend logic rather than the frontend authentication flow.

If you are interested in setting up a full dashboard experience, a tutorial is in the works. Follow me on twitter for updates @isaacwgarcia.

You can find more example repositories at the StepZen Github. If you're not already signed up for StepZen, you can sign up here and/or join the Discord Community.