Jamstack and the power of serverless with FaunaDB

In this article we will create a Jamstack website powered by Gatsby, Netlify Functions, Apollo and FaunaDB. Our site will use the Harry Potter API for its data that will be stored in a FaunaDB database. The data will be accessed using serverless functions and Apollo. Finally we will display our data in a Gatsby site styled using Theme-ui.

This finished site will look a little something like this: serverless-graphql-potter.netlify.app/

We will begin by focusing on what these technologies are and why, as frontend developers, we should be leveraging them. We will then begin our project and create our schema.

The Jamstack

Jamstack is a term often used to describe sites that are served as static assets to a CDN, of course this is nothing new, anyone who has made a simple site with HTML and CSS and published it has served a static site. To walk away thinking that the only purpose of Jamstack sites are to serve static files would be doing it a great injustice and miss some of the awesome things this "new" way of building web apps provides.

A few of the benefits of going Jamstack

  • High security and more secure. Fewer points of attack due to static files and external APIs served over CDN
  • Cheaper hosting and easier scalability with serverless functions
  • Fast! Pre-built assets served from a CDN instead of a server

A popular way of storing the data your site requires, apart from as markdown files, is the use of a headless CMS (Content Management System). These CMSs have adopted the term headless as they don't come with their own frontend that displays the data stored, like Wordpress for example. Instead they are headless, they have no frontend.

A headless CMS can be set up so that once a change to the data is made in the CMS a new build is triggered via a webhook (just one way of doing it, you could trigger rebuilds other ways) and the site will be deployed again with the new data.

As an example we could have some images stored in our CMS that are pulled into our site via a graphql query and shown on our site. If we wanted to change one of our images we could do so via our CMS which would then trigger a new build on publish and the new image would then be visible on our site.

There are many great options to choose from when considering which CMS to use:

  • Netlify CMS
  • Contenful
  • Sanity.io
  • Tina CMS
  • Butter CMS

The potential list is so long i will point you in the direction of a great site that lists most of them headlesscms.org!

For more information and a great overview of what the Jamstack is and some more of its benefits i recommend checking out jamstack.org.

Just because our site is served as static assets, that doesn't mean we cant work in a dynamic way and have the benefits of dynamic data! We wont be diving deep into all of its benefits, but we will be looking at how we can take our static site and make it dynamic by way of taking a serverless approach to handling our data through AWS Lambda functions, which we will use via Netlify and FaunaDB.

Serverless

Back in the old days, long long ago before we spread our stack with jam, we had a website that was a combination of HTML markup, CSS styling and JavaScript. Our website gave our user data to access and manipulate and our data was stored in a database which was hosted on a server. If we hosted this database ourselves we were responsible for keeping it going and maintaining it and all of its stored data. Our database could hold only a certain amount of data which meant that if we were lucky enough to get a lot of traffic it would soon struggle to handle all of the requests coming its way and so our end users might experience some downtime or no data at all.

If we paid for a hosted server then we were paying for the up time even when no requests were being sent.

To counter these issues serverless computing was introduced. Now, lets cut through all the magic this might imply and simply state that serverless still involves servers, the big difference is that they are hosted in the cloud and execute some code for us.

Providing the requested resources as a simple function they only run when that request is made. This means that we are only charged for the resources and time the code is running for. With this approach we have done away with the need to pay a server provider for constant up time, which is one of the big plus points of going serverless.

Being able to scale up and down is also a major benefit of using serverless functions to interact with our data stores. In a nutshell this means that as multiple requests come in via our serverless functions, our cloud provider can create multiple instances of the same function to handle those requests and run them in parallel. One downside to this is the concept of cold starts where because our functions are spun up on demand they need a small amount of time to start up which can delay our response. However, once up if multiple requests are received our serverless functions will stay open to requests and handle them before closing down again.

FaunaDB

FaunaDB is a global serverless database that has native graphql support, is multi tenancy which allows us to have nested databases and is low latency from any location. Its also one of the only serverless databases to follow the ACID transactions which guarantee consistent reads and writes to the database.

Fauna also provides us with a High Availability solution with each server globally located containing a partition of our database, replicating our data asynchronously with each request with a copy of our database or the transaction made.

Some of the benefits to using Fauna can be summarized as:

  • Transactional
  • Multi-document
  • Geo-distributed

In short, Fauna frees the developer from worry about single or multi-document solutions. Guarantees consistent data without burdening the developer on how to model their system to avoid consistency issues. To get a good overview of how Fauna does this see this blog post about the FaunaDB distributed transaction protocol.

There are a few other alternatives that one could choose instead of using Fauna such as:

  • Firebase
  • Cassandra
  • MongoDB

But these options don't give us the ACID guarantees that Fauna does, compromising scaling.

ACID

  • Atomic - all transactions are a single unit of truth, either they all pass or none. If we have multiple transactions in the same request then either both are good or neither are, one cannot fail and the other succeed.
  • Consistent - A transaction can only bring the database from one valid state to another, that is, any data written to the database must follow the rules set out by the database, this ensures that all transactions are legal.
  • Isolation - When a transaction is made or created, concurrent transactions leave the state of the database the same as is they would be if each request was made sequentially.
  • Durability - Any transaction that is made and committed to the database is persisted in the the database, regardless of down time of the system or failure.

Now that we have a good overview of the stack we will be using lets get to the code!

Setup project

We'll create a new folder to house our project, initialize it with yarn and add some files and folders to that we will be working with throughout.

At the projects root create a functions folder with a nested graphql folder. In that folder we will create three files, our graphql schema which we will import into Fauna, our serverless function which will live in graphql.js and create the link to and use the schema from Fauna and our database connection to Fauna.

GNU Bash icon
mkdir harry-potter
cd harry-potter
yarn init- y
mkdir src/pages/
cd src/pages && touch index.js
mkdir src/components
touch gatsby-config.js
touch gatsby-browser.js
touch gatsby-ssr.js
touch .gitignore
mkdir functions/graphql
cd functions/graphql && touch schema.gql graphql.js db-connection.js

We'll also need to add some packages.

GNU Bash icon
yarn add gatsby react react-dom theme-ui gatsby-plugin-theme-ui faunadb isomorphic-fetch dotenv

Add the following to your newly created .gitignore file:

GNU Bash icon
.netlify
node_modules
.cache
public

Serverless setup

Lets begin with our schema. We are going to take advantage of an awesome feature of Fauna. By creating our schema and importing it into Fauna we are letting it take care of a lot of code for us by auto creating all the classes, indexes and possible resolvers.

schema.gql

JavaScript icon
type Query {
allCharacters: [Character]!
allSpells: [Spell]!
}
type Character {
name: String!
house: String
patronus: String
bloodStatus: String
role: String
school: String
deathEater: Boolean
dumbledoresArmy: Boolean
orderOfThePheonix: Boolean
ministryOfMagic: Boolean
alias: String
wand: String
boggart: String
animagus: String
}
type Spell {
effect: String
spell: String
type: String
}

Our schema is defining the shape of the data that we will soon be seeding into the data from the Potter API. Our top level query will return two things, an array of Characters and an array of Spells. We have then defined our Character and Spell types. We don't need to specify an id here as when we seed the data from the Potter API we will attach it then.

Now that we have our schema we can import it into Fauna. Head to your fauna console and navigate to the graphql tab on the left, click import schema and find the file we just created, click import and prepare to be amazed!

Once the import is complete we will be presented with a graphql playground where we can run queries against our newly created database using its schema. Alas, we have yet to add any data, but you can check the collections and indexes tabs on the left of the console and see that fauna has created two new collections for us, Character and Spell.

A collection is a grouping of our data with each piece of data being a document. Or a table with rows if you are coming from an SQL background. Click the indexes tab to see our two new query indexes that we specified in our schema, allCharacters and allSpells. db-connection.js

Inside db-connection.js we will create the Fauna client connection, we will use this connection to seed data into our database.

JavaScript icon
require('dotenv').config();
const faunadb = require('faunadb');
const query = faunadb.query;
function createClient() {
if (!process.env.FAUNA_ADMIN) {
throw new Error(
`No FAUNA_ADMIN key in found, please check your fauna dashboard or create a new key.`,
);
}
const client = new faunadb.Client({
secret: process.env.FAUNA_ADMIN,
});
return client;
}
exports.client = createClient();
exports.query = query;

Here we are creating a function which will check to see if we have an admin key from our Fauna database, if none is found we are returning a helpful error message to the console. If the key is found we are creating a connection to our Fauna database and exporting that connection from file. We are also exporting the query variable from Fauna as that will allow us to use some FQL (Fauna Query Language) when seeding our data.

Head over to your Fauna console and click the security tab, click new key and select admin from the role dropdown. The admin role will allow us to manage the database, in our case, seed data into it. Choose the name FAUNA_ADMIN and hit save. We will need to create another key for use in using our stored schema from Fauna. Select server for the role of this key and name it SERVER_KEY. Don't forget to make a note of the keys before you close the windows as you wont be able to view them again!

That’s a great start. Next up we will seed our data and begin implementing our frontend!

Now that we have our keys its time to grab one more, from the Potter API, it's as simple as hitting the get key button in the top right hand corner of the page, make a note of it and head back to your code editor.

We don't want our keys getting into the wrong wizards hands so lets store them as environment variables. Create a .env file at the projects root and add add them. Also add the .env path to the .gitignore file.

.gitignore

GNU Bash icon
// ...other stuff
.env.*

.env

GNU Bash icon
FAUNA_ADMIN=xxxxxxxxxxxxxxxxxxxxxxxxxxx
SERVER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxx
POTTER_KEY=xxxxxxxxxxxxxxxxxxxxxxxx

Our database isn't much good if it doesn't have any data in it, lets change that! Create a file at the projects root and name it seed.js

JavaScript icon
const fetch = require('isomorphic-fetch');
const { client, query } = require('./functions/graphql/db');
const q = query;
const potterEndPoint = `https://www.potterapi.com/v1/characters/?key=${process.env.POTTER_KEY}`;
fetch(potterEndPoint)
.then((res) => res.json())
.then((res) => {
console.log({ res });
const characterArray = res.map((char, index) => ({
_id: char._id,
name: char.name,
house: char.house,
patronus: char.patronus,
bloodStatus: char.blood,
role: char.role,
school: char.school,
deathEater: char.deathEater,
dumbledoresArmy: char.dumbledoresArmy,
orderOfThePheonix: char.orderOfThePheonix,
ministryOfMagic: char.ministryOfMagic,
alias: char.alias,
wand: char.wand,
boggart: char.boggart,
animagus: char.animagus,
}));
client
.query(
q.Map(
characterArray,
q.Lambda(
'character',
q.Create(q.Collection('Character'), { data: q.Var('character') }),
),
),
)
.then(console.log('Wrote potter characters to FaunaDB'))
.catch((err) => console.log('Failed to add characters to FaunaDB', err));
});

There is quite a lot going on here so lets break it down.

  • We are importing fetch to do a post against the potter endpoint
  • We import our Fauna client connection and the query variable which holds the functions need to create the documents in our collection.
  • We call the potter endpoint and map over the result, adding all the data we require (which also corresponds to the schema we create earlier).
  • Using our Fauna client we use FQL to first map over the new array of characters, we then call a lambda function (an anonymous function) and choose a variable name for each row instance and create a new document in our Character collection.
  • If all was successful we return a message to the console, if unsuccessful we return the error.

From the projects root run our new script.

GNU Bash icon
node seed.js

If you now take a look inside the collections tab in the Fauna console you will see that the database has populated with all the characters from the potterverse! Click on one of the rows (documents) and you can see the data.

We will create another seed script to get our spells data into our database. Run the script and check out the Spell collections tab to view all the spells.

JavaScript icon
const fetch = require('isomorphic-fetch');
const { client, query } = require('./functions/graphql/db');
const q = query;
const potterEndPoint = `https://www.potterapi.com/v1/spells/?key=${process.env.POTTER_KEY}`;
fetch(potterEndPoint)
.then((res) => res.json())
.then((res) => {
console.log({ res });
const spellsArray = res.map((char, index) => ({
_id: char._id,
effect: char.effect,
spell: char.spell,
type: char.type,
}));
client
.query(
q.Map(
spellsArray,
q.Lambda(
'spell',
q.Create(q.Collection('Spell'), { data: q.Var('spell') }),
),
),
)
.then(console.log('Wrote potter spells to FaunaDB'))
.catch((err) => console.log('Failed to add spells to FaunaDB', err));
});
GNU Bash icon
node seed-spells.js

Now that we have data in our database its time to create our serverless function which will pull in our schema from Fauna.

graphql.js

JavaScript icon
require('dotenv').config();
const { createHttpLink } = require('apollo-link-http');
const {
ApolloServer,
makeRemoteExecutableSchema,
introspectSchema,
} = require('apollo-server-micro');
const fetch = require('isomorphic-fetch');
const link = createHttpLink({
uri: 'https://graphql.fauna.com/graphql',
fetch,
headers: {
Authorization: `Bearer ${process.env.SERVER_KEY}`,
},
});
const schema = makeRemoteExecutableSchema({
schema: introspectSchema(link),
link,
});
const server = new ApolloServer({
schema,
introspection: true,
});
exports.handler = server.createHandler({
cors: {
origin: '*',
credentials: true,
},
});

Lets go through what we just did.

  • We created a link to Fauna using the createHttpLink function which takes our Fauna graphql endpoint and attaches our server key to the header. This will fetch the graphql results from the endpoint over an http connection.
  • We then grab our schema from Fauna using the makeRemoteExecutableSchema function by passing the link to the introspectSchema function, we also provide the link.
  • A new ApolloServer instance is then created and our schema passed in.
  • Finally we export our handler as Netlify requires us to do when writing serverless functions.
  • Note that we might, and most probably will, run into CORS issues when trying to fetch our data so we pass our createHandler function the cors option, setting its origin to anything and credentials as true.

Using our data!

Before we can think about displaying our data we must first do some tinkering. We will be using some handy hooks from Apollo for querying our (namely useQuery) and for that to work we must first set up our provider, which is similar to Reacts context provider. We will wrap our sites root with this provider and pass in our client, thus making it available throughout our site. To wrap the root element in a Gatsby site we must use the gatsby-browser.js and gatsby-ssr.js files. The implementation will be identical in both.

gatsby-browser.js && gatsby-ssr.js

We will have to add a few more packages at this point:

GNU Bash icon
yarn add @apollo/client apollo-link-context
React icon
const React = require('react');
const {
ApolloProvider,
ApolloClient,
InMemoryCache,
} = require('@apollo/client');
const { setContext } = require('apollo-link-context');
const { createHttpLink } = require('apollo-link-http');
const fetch = require('isomorphic-fetch');
const httpLink = createHttpLink({
uri: 'https://graphql.fauna.com/graphql',
fetch,
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: `Bearer ${process.env.SERVER_KEY}`,
},
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
export const wrapRootElement = ({ element }) => (
<ApolloProvider client={client}>{element}</ApolloProvider>
);

There are other ways of setting this up, i had originally just created an ApolloClient instance and passed in the Netlify functions url as a http link then passed that down to the provider but i was encountering authorization issues, with a helpful message stating that the request lacked authorization headers. The solution was to send the authorization along with a header on every http request.

Lets take a look at what we have here:

  • Created a new http link much the same as we did before when creating our server instance.
  • Create an auth link which returns the headers to the context so the http link can read them. Here we pass in our Fauna key with server rights.
  • Then we create the client to be passed to the provider with the link now set as the auth link.

Now that we have the nuts and bolts all setup we can move onto some frontend code!

Make it work then make it pretty!

We'll also want to create some base components. We'll be using a Gatsby layout plugin to make life easier for us. We'll also utilize some google fonts via a plugin. Stay with me...

GNU Bash icon
mkdir -p src/layouts/index.js
cd src/components && touch header.js
cd src/components && touch main.js
cd src/components && touch footer.js
yarn add gatsby-plugin-layout
yarn add gatsby-plugin-google-fonts

Now we need to add the theme-ui, layout and google fonts plugins to our gatsby-config.js file:

JavaScript icon
module.exports = {
plugins: [
{
resolve: 'gatsby-plugin-google-fonts',
options: {
fonts: ['Muli', 'Open Sans', 'source sans pro:300,400,400i,700'],
},
},
{
resolve: 'gatsby-plugin-layout',
options: {
component: require.resolve('./src/layouts/index.js'),
},
},
'gatsby-plugin-theme-ui',
],
};

We'll begin with our global layout. This will include a css reset and render our header component and any children, which in our case is the rest of the applications pages/components.

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import React from 'react';
import { Global, css } from '@emotion/core';
import Header from './../components/site/header';
const Layout = ({ children, location }) => {
return (
<>
<Global
styles={css`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Track */
::-webkit-scrollbar-track {
background: #fff;
border-radius: 20px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #000;
border-radius: 20px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #000;
}
}
body {
scroll-behavior: smooth;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
width: 100%;
overflow-x: hidden;
height: 100%;
}
`}
/>
<Header location={location} />
{children}
</>
);
};
export default Layout;

Because we are using gatsby-plugin-layout our layout component will be wrapped around all of our pages so that we can skip importing it ourselves. For our site its a trivial step as we could just as easily import it but for more complex layout solutions this can come in real handy.

To provide an easy way to style our whole site through changing just a few variables we can utilize gatsby-plugin-theme-ui.

This article wont cover the specifics of how to use theme-ui, for that i suggest going over another tutorial i have written which covers the hows and whys how-to-make-a-gatsby-ecommerce-theme-part-1/

GNU Bash icon
cd src && mkdir gatsby-plugin-theme-ui && touch index.js

In this file we will create our sites styles which we will be able to access via the theme-ui sx prop.

JavaScript icon
export default {
fonts: {
body: 'Open Sans',
heading: 'Muli',
},
fontWeights: {
body: 300,
heading: 400,
bold: 700,
},
lineHeights: {
body: '110%',
heading: 1.125,
tagline: '100px',
},
letterSpacing: {
body: '2px',
text: '5px',
},
colors: {
text: '#FFFfff',
background: '#121212',
primary: '#000010',
secondary: '#E7E7E9',
secondaryDarker: '#545455',
accent: '#DE3C4B',
},
breakpoints: ['40em', '56em', '64em'],
};

Much of this is self explanatory, the breakpoints array is used to allow us to add responsive definitions to our inline styles via the sx prop. For example:

React icon
<p
sx={{
fontSize: ['0.7em', '0.8em', '1em'],
}}
>
Some text here...
</p>

The font size array indexes corresponded to our breakpoints array set in our theme-ui index file. Next we'll create our header component. But before we do we must install another package, i'll explain why once you see the component.

GNU Bash icon
yarn add @emotion/styled
cd src/components
mkdir site && touch header.js

header.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import HarryPotterLogo from '../../assets/svg-silhouette-harry-potter-4-transparent.svg.svg';
import { Link } from 'gatsby';
import styled from '@emotion/styled';
const PageLink = styled(Link)`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const Header = ({ location }) => {
return (
<section
sx={{
gridArea: 'header',
justifyContent: 'flex-start',
alignItems: 'center',
width: '100%',
height: '100%',
display: location.pathname === '/' ? 'none' : 'flex',
}}
>
<Link to="/">
<HarryPotterLogo
sx={{
height: '100px',
width: '100px',
padding: '1em',
}}
/>
</Link>
<PageLink
sx={{
fontFamily: 'heading',
fontSize: '2em',
color: 'white',
marginRight: '2em',
}}
to="/houses"
>
houses
</PageLink>
<PageLink
sx={{
fontFamily: 'heading',
fontSize: '2em',
color: 'white',
}}
to="/spells"
>
Spells
</PageLink>
</section>
);
};
export default Header;

Lets understand our imports first.

  • We have imported and used the jsx pragma from theme-ui to allow to to style our elements and components inline with the object syntax
  • The HarryPotterLogo is a logo i found via google which was placed in a folder named assets inside of our src folder. Its an svg which we alter the height and width of using the sx prop.
  • Gatsby link is needed for us to navigate between pages in our site.

You may be wondering why we have installed emotion/styled when we could just use the sx prop, like we have done else where... Well the answer lies in the affect we are using on the page links.

The sx prop doesn’t seem to have access to, or i should say perhaps that its doesn't have in its definitions, the -webkit-background-clip property which we are using to add a cool linear-gradient affect on hover. For this reason we have pulled the logic our into a new component called PageLink which is a styled Gatsby Link. With styled components we can use regular css syntax and as such have access to the -webkit-background-clip property.

The header component is taking the location prop provided by @reach/router which Gatsby uses under the hood for its routing. This is used to determine which page we are on. Due to the fact that we have a different layout for our main home page and the rest of the site we simply use the location object to check if we are on the home page, if we are we set a display none to hide the header component.

The last thing we need to do is set our grid areas which we will be using in later pages. This is just my preferred way of doing it, but i like the separation. Create a new folder inside of src called window and add an index.js file.

JavaScript icon
export const HousesSpellsPhoneTemplateAreas = `
'header'
'main'
'main'
`;
export const HousesSpellsTabletTemplateAreas = `
'header header header header'
'main main main main'
`;
export const HousesSpellsDesktopTemplateAreas = `
'header header header header'
'main main main main'
`;
export const HomePhoneTemplateAreas = `
'logo'
'logo'
'logo'
'author'
'author'
'author'
'author'
`;
export const HomeTabletTemplateAreas = `
'logo . . '
'logo author author'
'logo author author'
'. . . '
`;
export const HomeDesktopTemplateAreas = `
'logo . . '
'logo author author'
'logo author author'
'. . . '
`;

Cool, now we have our global layout complete, lets move onto our home page. Open up the index.js file inside of src/pages and add the following:

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import React from 'react';
import {
HomePhoneTemplateAreas,
HomeTabletTemplateAreas,
HomeDesktopTemplateAreas,
} from './../window/index';
import LogoSection from './../components/site/logo-section';
import AuthorSection from '../components/site/author-section';
export default () => {
return (
<div
sx={{
width: '100%',
height: '100%',
maxWidth: '1200px',
margin: '1em',
}}
>
<div
sx={{
display: 'grid',
gridTemplateColumns: ['1fr', '500px 1fr', '500px 1fr'],
gridAutoRows: '100px 1fr',
gridTemplateAreas: [
HomePhoneTemplateAreas,
HomeTabletTemplateAreas,
HomeDesktopTemplateAreas,
],
width: '100%',
height: '100vh',
background: '#1E2224',
maxWidth: '1200px',
}}
>
<LogoSection />
<AuthorSection />
</div>
</div>
);
};

This is the first page our visitors will see. We are using a grid to compose our layout of the page and utilizing the responsive array syntax in our grid-template-columns and areas properties. To recap how this works we can take a closer look at the gridTemplateAreas property and see that the first index is for phone (or mobile if you will) with the second being tablet and the third desktop. We could add more if we so wished but these will suffice for our needs.

Lets move on to creating our logo section. In src/components/site create two new files called logo.js and logo-section.js

logo.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import HarryPotterLogo from '../assets/svg-silhouette-harry-potter-4-transparent.svg.svg';
export const Logo = () => (
<HarryPotterLogo
sx={{
height: ['200px', '300px', '500px'],
width: ['200px', '300px', '500px'],
padding: '1em',
position: 'relative',
}}
/>
);

Our logo is the Harry Potter svg mentioned earlier. You can of course choose whatever you like as your sites logo. This one is merely “HR” in a fancy font.

logo-section.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import { Logo } from '../logo';
const LogoSection = () => {
return (
<section
sx={{
gridArea: 'logo',
display: 'flex',
alignItems: 'center',
justifyContent: ['start', 'center', 'center'],
position: 'relative',
width: '100%',
}}
>
<Logo />
</section>
);
};
export default LogoSection;

Next up is our author section which will site next to our logo section Create a new file inside of src/components/site called author-section.js

author-section.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import { Link } from 'gatsby';
import { houseEmoji, spellsEmoji } from './../../helpers/helpers';
import styled from '@emotion/styled';
import { wizardEmoji } from './../../helpers/helpers';
const InternalLink = styled(Link)`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const ExternalLink = styled.a`
color: #fff;
&:hover {
background-image: linear-gradient(
90deg,
rgba(127, 9, 9, 1) 0%,
rgba(255, 197, 0, 1) 12%,
rgba(238, 225, 23, 1) 24%,
rgba(0, 0, 0, 1) 36%,
rgba(13, 98, 23, 1) 48%,
rgba(170, 170, 170, 1) 60%,
rgba(0, 10, 144, 1) 72%,
rgba(148, 119, 45, 1) 84%
);
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const AuthorSection = () => {
return (
<section
sx={{
gridArea: 'author',
position: 'relative',
margin: '0 auto',
}}
>
<h1
sx={{
fontFamily: 'heading',
color: 'white',
letterSpacing: 'text',
fontSize: ['3em', '3em', '5em'],
}}
>
Serverless Potter
</h1>
<div
sx={{
display: 'flex',
justifyContent: 'start',
alignItems: 'flex-start',
width: '300px',
marginTop: '3em',
}}
>
<InternalLink
sx={{
fontFamily: 'heading',
fontSize: '2.5em',
// color: 'white',
marginRight: '2em',
}}
to="/houses"
>
Houses
</InternalLink>
<InternalLink
sx={{
fontFamily: 'heading',
fontSize: '2.5em',
color: 'white',
}}
to="/spells"
>
Spells
</InternalLink>
</div>
<p
sx={{
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
color: 'white',
marginTop: '2em',
width: ['300px', '500px', '900px'],
}}
>
This is a site that goes with the tutorial on creating a jamstack site
with serverless functions and FaunaDB I decided to use the potter api as
i love the world of harry potter {wizardEmoji}
</p>
<p
sx={{
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
color: 'white',
marginTop: '1em',
width: ['300px', '500px', '900px'],
}}
>
Built with Gatsby, Netlify functions, Apollo and FaunaDB. Data provided
via the Potter API.
</p>
<p
sx={{
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
color: 'white',
marginTop: '1em',
width: ['300px', '500px', '900px'],
}}
>
Select <strong>Houses</strong> or <strong>Spells</strong> to begin
exploring potter stats!
</p>
<div
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<ExternalLink
sx={{
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
color: 'white',
marginTop: '1em',
width: ['300px', '500px', '900px'],
}}
href="your-personal-website"
>
author: your name here!
</ExternalLink>
<ExternalLink
sx={{
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
color: 'white',
marginTop: '1em',
width: '900px',
}}
href="your-github-repo-for-this-project"
>
github: the name you gave this project
</ExternalLink>
</div>
</section>
);
};
export default AuthorSection;

This component outlines what the project is, displays links to the other pages and the projects repository. You can change the text I’ve added, this was just for demo purposes. As you can see, we are again using emotion/styled as we are making use of the -webkit-background-clip property on our cool linear-gradient links. We have two here, one for external links, which uses the a tag, and another for internal link which uses Gatsby Link. Note that you should always use the traditional HTML a tag for external links and the Gatsby Link to configure your internal routing.

You may also notice that there is an import from a helper file what exports some emojis. Lets take a look at that. Create a new folder inside of src.

GNU Bash icon
cd src
mkdir helpers && touch helpers.js

helpers.js

JavaScript icon
export const gryffindorColors =
'linear-gradient(90deg, rgba(127,9,9,1) 27%, rgba(255,197,0,1) 61%)';
export const hufflepuffColors =
'linear-gradient(90deg, rgba(238,225,23,1) 35%, rgba(0,0,0,1) 93%)';
export const slytherinColors =
'linear-gradient(90deg, rgba(13,98,23,1) 32%, rgba(170,170,170,1) 69%)';
export const ravenclawColors =
'linear-gradient(90deg, rgba(0,10,144,1) 32%, rgba(148,107,45,1) 69%)';
export const houseEmoji = `🏡`;
export const spellsEmoji = `💫`;
export const wandEmoji = `💫`;
export const patronusEmoji = ``;
export const deathEaterEmoji = `🐍`;
export const dumbledoresArmyEmoji = `⚔️`;
export const roleEmoji = `📖`;
export const bloodStatusEmoji = `🧙🏾‍♀️ 🤵🏾`;
export const orderOfThePheonixEmoji = `🦄`;
export const ministryOfMagicEmoji = `📜`;
export const boggartEmoji = `🕯`;
export const aliasEmoji = `👨🏼‍🎤`;
export const wizardEmoji = `🧙🏼‍♂️`;
export const gryffindorEmoji = `🦁`;
export const hufflepuffEmoji = `🦡`;
export const slytherinEmoji = `🐍`;
export const ravenclawEmoji = `🦅`;
export function checkNull(value) {
return value !== null ? value : 'unknown';
}
export function checkDeathEater(value) {
if (value === false) {
return 'no';
}
return 'undoubtedly';
}
export function checkDumbledoresArmy(value) {
if (value === false) {
return 'no';
}
return `undoubtedly ${wizardEmoji}`;
}

The emojis were taken from a really cool site called Emoji Clipboard, it lets you search and literally copy paste the emojis! We’ll be using these emojis in our cards to display the characters from Harry Potter. As well as the emojis we have some utility functions that will also be used in the cards. Each house in Harry Potter has a set of colors that sets them apart form the other houses. These we have exported as linear-gradients for later use.

Nice! We are nearly there but we haven’t quite finished yet! Next we will use our data and display it to the user of our site!

We have done quite a bit of setup but haven’t yet had a chance to use our data that we have saved in our Fauna database. Now’s the time to bring in Apollo and put together a page that shows all the characters data for each house. We are also going to implement a simple searchbar to allow the user to search the characters of each house!

Inside src/pages create a new file called houses.js and add the following:

houses.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import React from 'react';
import { gql, useQuery } from '@apollo/client';
import MainSection from './../components/site/main-section';
import {
HousesPhoneTemplateAreas,
HousesTabletTemplateAreas,
HousesDesktopTemplateAreas,
} from '../window';
const GET_CHARACTERS = gql`
query GetCharacters {
allCharacters {
data {
_id
name
house
patronus
bloodStatus
role
school
deathEater
dumbledoresArmy
orderOfThePheonix
ministryOfMagic
alias
wand
boggart
animagus
}
}
}
`;
const Houses = () => {
const {
loading: characterLoading,
error: characterError,
data: characterData,
} = useQuery(GET_CHARACTERS);
const [selectedHouse, setSelectedHouse] = React.useState([]);
React.useEffect(() => {
const gryffindor =
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Gryffindor',
);
setSelectedHouse(gryffindor);
}, [characterLoading, characterData]);
const getHouse = (house) => {
switch (house) {
case 'gryffindor':
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Gryffindor',
),
);
break;
case 'hufflepuff':
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Hufflepuff',
),
);
break;
case 'slytherin':
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Slytherin',
),
);
break;
case 'ravenclaw':
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Ravenclaw',
),
);
break;
default:
setSelectedHouse(
!characterLoading &&
!characterError &&
characterData.allCharacters.data.filter(
(char) => char.house === 'Gryffindor',
),
);
break;
}
};
return (
<div
sx={{
gridArea: 'main',
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, auto))',
gridAutoRows: 'auto',
gridTemplateAreas: [
HousesSpellsPhoneTemplateAreas,
HousesSpellsTabletTemplateAreas,
HousesSpellsDesktopTemplateAreas,
],
width: '100%',
height: '100%',
position: 'relative',
}}
>
<MainSection house={selectedHouse} getHouse={getHouse} />
</div>
);
};
export default Houses;

We are using @apollo/client from which we import gql to construct our graphql query and the useQuery hook which will take care of handling the state of the returned data for us. This handy hook returns three states:

  • loading - Is the data currently loading?
  • error - If there was an error we will get it here
  • data - The requested data

Our page will be handling the currently selected house so we use the React useState hook and initialize it with an empty array on first render. There after we use the useEffect hook to set the initial house as Gryffindor (because Gryffindor is best. Fight me!) The dependency array takes in the loading and data states.

We then have a function which returns a switch statement (I know not everyone likes these but i do and i find that they are simple to read and understand). This function checks the currently selected house and if there are no errors in the query it loads the data from that house into the selected house state array. This function is passed down to another component which uses that data to display the house characters in a grid of cards.

Lets create that component now. Inside src/components/site create a new file called main-section.js

main-section.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import React from 'react';
import Card from '../cards/card';
import SearchBar from './searchbar';
import { useSearchBar } from './useSearchbar';
import Loading from './loading';
import HouseSection from './house-section';
const MainSection = React.memo(({ house, getHouse }) => {
const { members, handleSearchQuery } = useSearchBar(house);
return house.length ? (
<div
sx={{
gridArea: 'main',
height: '100%',
position: 'relative',
}}
>
<div
sx={{
color: 'white',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2em',
position: 'relative',
}}
>
<h4>
{house[0].house} Members - {house.length}
</h4>
<SearchBar handleSearchQuery={handleSearchQuery} />
<HouseSection getHouse={getHouse} />
</div>
<section
sx={{
margin: '0 auto',
width: '100%',
display: 'grid',
gridAutoRows: 'auto',
gridTemplateColumns: 'repeat(auto-fill, minmax(auto, 500px))',
gap: '1.5em',
justifyContent: 'space-evenly',
marginTop: '1em',
position: 'relative',
height: '100vh',
}}
>
{members.map((char, index) => (
<Card key={char._id} index={index} {...char} />
))}
</section>
</div>
) : (
<Loading />
);
});
export default MainSection;

Our main section is wrapped in memo, which means that React will render the component and memorize the result. If the next time the props are passed in and they are the same, React will use the memorized result and skip re-rendering the component again. This is helpful as our component will be re-rendering a lot as the user changes houses or uses the searchbar, which will will soon create.

In fact, lets do do that now. We will be creating a search bar component and a custom hook to handle the search logic. Inside src/components/site create two new files. searchbar.js and useSearchbar.js

searchbar.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
const SearchBar = ({ handleSearchQuery }) => {
return (
<div
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: '2em',
}}
>
<input
sx={{
color: 'greyBlack',
fontFamily: 'heading',
fontSize: '0.8em',
fontWeight: 'bold',
letterSpacing: 'body',
border: '1px solid',
borderColor: 'accent',
width: '300px',
height: '50px',
padding: '0.4em',
}}
type="text"
id="members-searchbar"
placeholder="Search members.."
onChange={handleSearchQuery}
/>
</div>
);
};
export default SearchBar;

Our searchbar takes in a search query function which is called when the input is used. The rest is just styling.

useSearchbar.js

React icon
import React from 'react';
export const useSearchBar = (data) => {
const emptyQuery = '';
const [searchQuery, setSearchQuery] = React.useState({
filteredData: [],
query: emptyQuery,
});
const handleSearchQuery = (e) => {
const query = e.target.value;
const members = data || [];
const filteredData = members.filter((member) => {
return member.name.toLowerCase().includes(query.toLowerCase());
});
setSearchQuery({ filteredData, query });
};
const { filteredData, query } = searchQuery;
const hasSearchResult = filteredData && query !== emptyQuery;
const members = hasSearchResult ? filteredData : data;
return { members, handleSearchQuery };
};

Our custom hook takes the selected house data as a prop. It has an internal state which holds an emptyQuery variable which we set to empty string and a filteredData array, set to empty. The function that runs in our searchbar is the following function declared in the hook. It takes the query as an event from the input, checks if the data provided to the hook has data or sets it to an empty array as a new variable called members. It then filters over the members array and checks if the query matches one of the characters names. Finally it sets the state with the returned filtered data and query.

We structure the state and create a new variable which checks if the state had any data or not. Finally returning the data, be that empty or not and the search function.

Phew! That was a lot to go over. Going back to our main section we can see that we are importing our new hook and passing in the selected house data, then destructing the members and search query function. The component checks if the house array has any length, if it does it returns the page. The page displays the current house, how many members the house has, the searchbar (which takes the search query function as a prop), a new house section which we will build and maps over the members returned from our custom hook.

In the house section we will make use of a super amazing library called Framer Motion. Lets first see how our new component looks and what it does.

In src/components/site create a new file called house-section.js

house-section.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import {
gryffindorColors,
hufflepuffColors,
slytherinColors,
ravenclawColors,
} from './../../helpers/helpers';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
const House = styled.a`
color: #fff;
&:hover {
background-image: ${(props) => props.house};
background-size: 100%;
background-repeat: repeat;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: bold;
}
`;
const HouseSection = ({ getHouse }) => {
return (
<section
sx={{
width: '100%',
position: 'relative',
}}
>
<ul
sx={{
listStyle: 'none',
cursor: 'crosshair',
fontFamily: 'heading',
fontSize: '1em',
display: 'flex',
flexDirection: ['column', 'row', 'row'],
alignItems: 'center',
justifyContent: 'space-evenly',
position: 'relative',
}}
>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
stiffness: 200,
damping: 20,
delay: 0.2,
}}
>
<House
onClick={() => getHouse('gryffindor')}
house={gryffindorColors}
>
Gryffindor
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
stiffness: 200,
damping: 20,
delay: 0.4,
}}
>
<House
onClick={() => getHouse('hufflepuff')}
house={hufflepuffColors}
>
Hufflepuff
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
stiffness: 200,
damping: 20,
delay: 0.6,
}}
>
<House onClick={() => getHouse('slytherin')} house={slytherinColors}>
Slytherin
</House>
</motion.li>
<motion.li
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: 'spring',
stiffness: 200,
damping: 20,
delay: 0.8,
}}
>
<House onClick={() => getHouse('ravenclaw')} house={ravenclawColors}>
Ravenclaw
</House>
</motion.li>
</ul>
</section>
);
};
export default HouseSection;

The purpose of this component is to show the user the four houses of Hogwarts, let them select a house and pass that selection back up to the main-section state. The component takes the getHouse function from main-section as a prop. We have created an internal link styled component , which takes each houses colours from our helper file, and returns the selected house on click.

Using framer motion we prepend each li with the motion tag. This allows us to add a simple scale animation by setting the initial value 0 (so it’s not visible), using the animate prop we say that it should animate in to it’s set size. The transition is specifying how the animation will work.

Back to the main-section component, we map over each member in the house and display their data in a Card component by spreading all the character data. Lets create that now.

Inside src/components/site create a new file called card.js

card.js

React icon
/** @jsx jsx */
import { jsx } from 'theme-ui';
import {
checkNull,
checkDeathEater,
checkDumbledoresArmy,
hufflepuffColors,
ravenclawColors,
gryffindorColors,
slytherinColors,
houseEmoji,
wandEmoji,
patronusEmoji,
bloodStatusEmoji,
ministryOfMagicEmoji,
boggartEmoji,
roleEmoji,
orderOfThePheonixEmoji,
deathEaterEmoji,
dumbledoresArmyEmoji,
aliasEmoji,
} from './../../helpers/helpers';
import { motion } from 'framer-motion';
const container = {
hidden: { scale: 0 },
show: {
scale: 1,
transition: {
delayChildren: 1,
},
},
};
const item = {
hidden: { scale: 0 },
show: { scale: 1 },
};
const Card = ({
_id,
name,
house,
patronus,
bloodStatus,
role,
deathEater,
dumbledoresArmy,
orderOfThePheonix,
ministryOfMagic,
alias,
wand,
boggart,
animagus,
index,
}) => {
return (
<motion.div variants={container} initial="hidden" animate="show">
<motion.div
variants={item}
sx={{
border: 'solid 2px',
borderImageSource:
house === 'Gryffindor'
? gryffindorColors
: house === 'Hufflepuff'
? hufflepuffColors
: house === 'Slytherin'
? slytherinColors
: house === 'Ravenclaw'
? ravenclawColors
: null,
borderImageSlice: 1,
display: 'flex',
flexDirection: 'column',
padding: '1em',
margin: '1em',
minWidth: ['250px', '400px', '500px'],
}}
>
<h2
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '2.5em',
borderBottom: 'solid 2px',
borderColor: 'white',
}}
>
{name}
</h2>
<div
sx={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: 'auto',
gap: '2em',
marginTop: '2em',
}}
>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>house:</strong> {house} {houseEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>wand:</strong> {checkNull(wand)} {wandEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>patronus:</strong> {checkNull(patronus)} {patronusEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>boggart:</strong> {checkNull(boggart)} {boggartEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>blood:</strong> {checkNull(bloodStatus)} {bloodStatusEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>role:</strong> {checkNull(role)} {roleEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>order of the pheonix:</strong>{' '}
{checkNull(orderOfThePheonix)} {orderOfThePheonixEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>ministry of magic:</strong>{' '}
{checkDeathEater(ministryOfMagic)} {ministryOfMagicEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>death eater:</strong> {checkDeathEater(deathEater)}{' '}
{deathEaterEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>dumbledores army:</strong>{' '}
{checkDumbledoresArmy(dumbledoresArmy)} {dumbledoresArmyEmoji}
</p>
<p
sx={{
color: 'white',
fontFamily: 'heading',
letterSpacing: 'body',
fontSize: '1.5em',
}}
>
<strong>alias:</strong> {checkNull(alias)} {aliasEmoji}
</p>
</div>
</motion.div>
</motion.div>
);
};
export default Card;

We are importing all of those cool emojis we added earlier in our helper file. The container and item objects are for use in our animations from framer motion. We descructure our props, of which there are many, and return a div which has the framer motion object prepended to it and the item object passed to the variants prop. This is a simpler way of passing the object and all of it’s values through. For certain properties we run a null check against them to determinate what we should show.

The only thing left to do is implement the Spells page and its associated components then the implementation of this site is done! Given all we have covered I’m sure you can handle the last part!

Your final result should resemble something like this: serverless-graphql-potter.

Did you notice the cool particles? That’s a nice touch you could add to your site!

Deploy the beast!

That’s a lot of code and we haven’t even checked that it works!! (of course during development you should check how things look and work and make changes accordingly, I didn’t cover running the site as that’s common practice while developing). Lets deploy our site to Netlify and check it out!

At the projects root create a new file called netlify.toml

netlify.toml

GNU Bash icon
[build]
command = "yarn build"
functions = "functions"
publish = "public"

If you don’t already have an account, create a new one at netlify.com. To publish your site:

  • Click create new site, identify yourself and choose your repository
  • set your build command as yarn build and publish directory as public
  • Click site settings and change site name and…. change the name!
  • On the left tab menu find build and deploy and click that and scroll down to the environment section and add your environment variables: SERVER_KEY and FAUNA_ADMIN
  • You can add the functions path under the functions tab but Netlify will also pick this up from the netlify.toml file you created

When you first created this new site Netlify tried to deploy it. It wouldn’t have worked as we hadn’t set the environment variables yet. Go to the deploys tab at the top of the page and hit the trigger deploy dropdown and deploy site. If you encounter any issues then please drop me an email at hello@richardhaines.dev and we can try and work through it together.

And that’s it! I hope you enjoyed it and learnt something along the way. Thank you for coming to my TED talk 😅

If you liked this article feel free to give me a follow on Twitter with the blue button at the top of the page. 😇