Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., @awscloud, former CTO @artsy, +@vestris, NYC

Email Twitter LinkedIn Github Strava
Creative Commons License

I recently needed to stand up a demo app that invoked an AWS AppSync GraphQL API. The existing endpoint (not available publicly) returned restaurant data for a given zip with the following schema.

type ZipData {
  zip: String
  timezone: String
  restaurants: [RestaurantData]
}

type RestaurantData {
  name: String
  latitude: String
  longitude: String
}

type Query {
  get_restaurants_by_zip(zip: String!): ZipData
}

schema {
  query: Query
}

The app I wanted to build was throwaway, and my goal was to make it happen effortlessly, during a single lunch break. I was told to try Redwood.js, AWS Amplify, Sanity.io, Next.js via prisma-examples, and some zero-code tools, including ReTool.

Redwood.js

Let’s start with Redwood.js …

Create an App

nvm use 12
yarn create redwood-app ./redwood-js-appsync-graphql-demo
cd ./redwood-js-appsync-graphql-demo

Add a Homepage

yarn redwood generate page home /

Add an Input Box for the Zip Code

When the form is submitted, state (including zip code) will change.

import { Link, routes } from '@redwoodjs/router'
import { useState } from 'react'
import { Form, TextField, Submit } from '@redwoodjs/forms'

const HomePage = () => {
  const [zip, setZip] = useState()

  const onSubmit = (data) => {
    setZip(data.zip)
  }

  return (
    <div>
      <Form onSubmit={onSubmit}>
        <TextField name="zip" placeholder="Zip code" maxLength="5" />
        <Submit>Go</Submit>
      </Form>
    </div>
  )
}

export default HomePage

Add graphql-request

yarn add -W graphql-request

Add the GraphQL Query

import { GraphQLClient } from 'graphql-request'

export const getRestaurantsByZip = async (zip) => {
  const endpoint = process.env.APPSYNC_API_ENDPOINT_URL

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      'x-api-key': process.env.APPSYNC_API_KEY
    },
  })

  const query = gql`query GetZip($zip: String!) {
    get_restaurants_by_zip(zip: $zip) {
      zip
      timezone
      restaurants {
        name
      }
    }
  }`

  return graphQLClient.request(query, { zip: zip })
}

Wire Up GraphQL Results

const onSubmit = (data) => {
  getRestaurantsByZip(data.zip).then(rc => {
    setZip(rc.get_restaurants_by_zip.zip)
  })
}

return (
  <div>
    ...
    <div>
      {zip &&
        <div>
          <h2>{zip.zip}</h2>
          <h3>{zip.timezone}</h3>
        </div>
      }
      {zip && zip.restaurants.map(r =>
        <div>{r.name}</div>
      )}
    </div>
  </div>
)

Not Taking Advantage of Redwood.js

So far I used Redwood.js similarly to how one would use Rails without models, views or controllers, just to host some Ruby code on a web page. Redwood has lots of features, including cells and side-loading. Seems like a missed opportunity! So I asked the Redwood.js community to help me leverage Redwood.js better. The answer is more generally described in the documentation under Server-Side API Integration.

Expose Restaurants Side-Loading

Add api/src/graphql/restaurants.sdl.js with the schema that our Redwood.js service will return.

import gql from 'graphql-tag'

export const schema = gql`
  type Restaurant {
    name: String
    longitude: String
    latitude: String
  }
  type Zip {
    zip: String
    timezone: String
    restaurants: [Restaurant]
  }
  type Query {
    restaurants(zip: String!): Zip
  }
` 

Fetch Data from the AppSync API

Wire up calls to the AppSync endpoint in api/src/lib/db.js.

import { GraphQLClient } from 'graphql-request'

export const request = async (query, variables) => {
  const endpoint = process.env.APPSYNC_API_ENDPOINT_URL

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      'x-api-key': process.env.APPSYNC_API_KEY
    },
  })

  return await graphQLClient.request(query, variables)
} 

Add api/src/services/restaurants.js that loads restaurant data using the above endpoint.

import { request } from 'src/lib/db'
import { gql } from 'graphql-request'

export const restaurants = async (args) => {
  const query = gql`query GetZip($zip: String!) {
    get_restaurants_by_zip(zip: $zip) {
      zip
      timezone
      restaurants {
        name
        longitude
        latitude
      }
    }
  }`

  var data = await request(query, args)
  return data.get_restaurants_by_zip
} 

Add a Restaurants Cell

yarn redwood generate cell restaurants

The cell makes a query to the Redwood.js service to fetch restaurants.

export const QUERY = gql`query($zip: String!) {
  restaurants(zip: $zip) {
    zip
    timezone
    restaurants {
      name
      longitude
      latitude
    }
  }
}`

export const Loading = () => <div>Loading ...</div>

export const Empty = () => <div>No zip yet!</div>

export const Failure = ({ error, zip }) => {
  return <div>Error loading zip {zip}: {error.message}</div>
}

export const Success = ({ restaurants }) => {
  return <div>
    <div>
      <h2>{restaurants.zip}</h2>
      <h3>{restaurants.timezone}</h3>
    </div>
    {restaurants.restaurants.map(r =>
      <div>
        <div>{r.name}</div>
      </div>
    )}
  </div>
}

Wire up the cell in the homepage.

const HomePage = () => {
  const [zip, setZip] = useState()

  const onSubmit = (data) => {
    setZip(data.zipCode)
  }

  return (
    <div>
      {zip && <RestaurantsCell zip={zip} />}
    </div>
  )
}

Put it All Together

Create a .env file with APPSYNC_API_ENDPOINT_URL and APPSYNC_API_KEY and run the app with yarn redwood dev.

See github.com/dblock/redwood-js-appsync-graphql-demo for the complete, working code, along with some Google maps.