The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
The key aspects of a Jamstack application are the following:
- The entire app runs on a CDN (or ADN). CDN stands for Content Delivery Network and an ADN is an Application Delivery Network.
- Everything lives in Git.
- Automated builds run with a workflow when developers push the code.
- There’s Automatic deployment of the prebuilt markup to the CDN/ADN.
- Reusable APIs make hasslefree integrations with many of the services. To take a few examples, Stripe for the payment and checkout, Mailgun for email services, etc. We can also write custom APIs targeted to a specific use-case. We will see such examples of custom APIs in this article.
- It’s practically Serverless. To put it more clearly, we do not maintain any servers, rather make use of already existing services (like email, media, database, search, and so on) or serverless functions.
In this article, we will learn how to build a Jamstack application that has:
- A global data store with GraphQL support to store and fetch data with ease. We will use Fauna to accomplish this.
- Serverless functions that also act as the APIs to fetch data from the Fauna data store. We will use Netlify serverless functions for this.
- We will build the client side of the app using a Static Site Generator called Gatsbyjs.
- Finally we will deploy the app on a CDN configured and managed by Netlify CDN.
So, what are we building today?
We all love shopping. How cool would it be to manage all of our shopping notes in a centralized place? So we’ll be building an app called ‘shopnote’ that allows us to manage shop notes. We can also add one or more items to a note, mark them as done, mark them as urgent, etc.
At the end of this article, our shopnote app will look like this,
TL;DR
We will learn things with a step-by-step approach in this article. If you want to jump into the source code or demonstration sooner, here are links to them.
- You can access the shop note demo from here: https://shopnote.netlify.app/
- All the source code used in this article is in my GitHub repo. Feel free to follow it, as I keep updating the source code frequently. https://github.com/atapas/shopnote
Set up Fauna
Fauna is the data API for client-serverless applications. If you are familiar with any traditional RDBMS, a major difference with Fauna would be, it is a relational NOSQL system that gives all the capabilities of the legacy RDBMS. It is very flexible without compromising scalability and performance.
Fauna supports multiple APIs for data-access,
- GraphQL: An open source data query and manipulation language. If you are new to the GraphQL, you can find more details from here, https://graphql.org/
- Fauna Query Language (FQL): An API for querying Fauna. FQL has language specific drivers which makes it flexible to use with languages like JavaScript, Java, Go, etc. Find more details of FQL from here.
In this article we will explain the usages of GraphQL for the ShopNote application.
First thing first, sign up using this URL. Please select the free plan which is with a generous daily usage quota and more than enough for our usage.
Next, create a database by providing a database name of your choice. I have used shopnotes as the database name.
After creating the database, we will be defining the GraphQL schema and importing it into the database. A GraphQL schema defines the structure of the data. It defines the data types and the relationship between them. With schema we can also specify what kind of queries are allowed.
At this stage, let us create our project folder. Create a project folder somewhere on your hard drive with the name, shopnote. Create a file with the name, shopnotes.gql
with the following content:
type ShopNote {
name: String!
description: String
updatedAt: Time
items: [Item!] @relation
}
type Item {
name: String!
urgent: Boolean
checked: Boolean
note: ShopNote!
}
type Query {
allShopNotes: [ShopNote!]!
}
Here we have defined the schema for a shopnote list and item, where each ShopNote
contains name, description, update time and a list of Items. Each Item
type has properties like, name, urgent, checked and which shopnote it belongs to.
Note the @relation
directive here. You can annotate a field with the @relation
directive to mark it for participating in a bi-directional relationship with the target type. In this case, ShopNote
and Item
are in a one-to-many relationship. It means, one ShopNote
can have multiple Items, where each Item can be related to a maximum of one ShopNote.
You can read more about the @relation
directive from here. More on the GraphQL relations can be found from here.
As a next step, upload the shopnotes.gql
file from the Fauna dashboard using the IMPORT SCHEMA button,
Upon importing a GraphQL Schema, FaunaDB will automatically create, maintain, and update, the following resources:
- Collections for each non-native GraphQL Type; in this case, ShopNote and Item.
- Basic CRUD Queries/Mutations for each Collection created by the Schema, e.g.
createShopNote
allShopNotes
; each of which are powered by FQL. - For specific GraphQL directives: custom Indexes or FQL for establishing relationships (i.e.
@relation
), uniqueness (@unique
), and more!
Behind the scene, Fauna will also help to create the documents automatically. We will see that in a while.
Fauna supports a schema-free object relational data model. A database in Fauna may contain a group of collections. A collection may contain one or more documents. Each of the data records are inserted into the document. This forms a hierarchy which can be visualized as:
Here the data record can be arrays, objects, or of any other supported types. With the Fauna data model we can create indexes, enforce constraints. Fauna indexes can combine data from multiple collections and are capable of performing computations.
At this stage, Fauna already created a couple of collections for us, ShopNote and Item. As we start inserting records, we will see the Documents are also getting created. We will be able view and query the records and utilize the power of indexes. You may see the data model structure appearing in your Fauna dashboard like this in a while,
Point to note here, each of the documents is identified by the unique ref
attribute. There is also a ts
field which returns the timestamp of the recent modification to the document. The data record is part of the data
field. This understanding is really important when you interact with collections, documents, records using FQL built-in functions. However, in this article we will interact with them using GraphQL queries with Netlify Functions.
With all these understanding, let us start using our Shopenotes database that is created successfully and ready for use.
Let us try some queries
Even though we have imported the schema and underlying things are in place, we do not have a document yet. Let us create one. To do that, copy the following GraphQL mutation query to the left panel of the GraphQL playground screen and execute.
mutation {
createShopNote(data: {
name: "My Shopping List"
description: "This is my today's list to buy from Tom's shop"
items: {
create: [
{ name: "Butther - 1 pk", urgent: true }
{ name: "Milk - 2 ltrs", urgent: false }
{ name: "Meat - 1lb", urgent: false }
]
}
}) {
_id
name
description
items {
data {
name,
urgent
}
}
}
}
Note, as Fauna already created the GraphQL mutation classes in the background, we can directly use it like, createShopNote
. Once successfully executed, you can see the response of a ShopNote creation at the right side of the editor.
The newly created ShopNote
document has all the required details we have passed while creating it. We have seen ShopNote
has a one-to-many relation with Item
. You can see the shopnote response has the item data nested within it. In this case, one shopnote has three items. This is really powerful. Once the schema and relation are defined, the document will be created automatically keeping that relation in mind.
Now, let us try fetching all the shopnotes. Here is the GraphQL query:
query {
allShopNotes {
data {
_id
name
description
updatedAt
items {
data {
name,
checked,
urgent
}
}
}
}
}
Let’s try the query in the playground as before:
Now we have a database with a schema and it is fully operational with creating and fetch functionality. Similarly, we can create queries for adding, updating, removing items to a shopnote and also updating and deleting a shopnote. These queries will be used at a later point in time when we create the serverless functions.
If you are interested to run other queries in the GraphQL editor, you can find them from here,
Create a Server Secret Key
Next, we need to create a secured server key to make sure the access to the database is authenticated and authorized.
Click on the SECURITY option available in the FaunaDB interface to create the key, like so,
On successful creation of the key, you will be able to view the key’s secret. Make sure to copy and save it somewhere safe.
We do not want anyone else to know about this key. It is not even a good idea to commit it to the source code repository. To maintain this secrecy, create an empty file called .env
at the root level of your project folder.
Edit the .env
file and add the following line to it (paste the generated server key in the place of, <YOUR_FAUNA_KEY_SECRET>
).
FAUNA_SERVER_SECRET=<YOUR_FAUNA_KEY_SECRET>
Add a .gitignore
file and write the following content to it. This is to make sure we do not commit the .env file to the source code repo accidentally. We are also ignoring node_modules as a best practice.
.env
We are done with all that had to do with Fauna’s setup. Let us move to the next phase to create serverless functions and APIs to access data from the Fauna data store. At this stage, the directory structure may look like this,
Set up Netlify Serverless Functions
Netlify is a great platform to create hassle-free serverless functions. These functions can interact with databases, file-system, and in-memory objects.
Netlify functions are powered by AWS Lambda. Setting up AWS Lambdas on our own can be a fairly complex job. With Netlify, we will simply set a folder and drop our functions. Writing simple functions automatically becomes APIs.
First, create an account with Netlify. This is free and just like the FaunaDB free tier, Netlify is also very flexible.
Now we need to install a few dependencies using either npm or yarn. Make sure you have nodejs installed. Open a command prompt at the root of the project folder. Use the following command to initialize the project with node dependencies,
npm init -y
Install the netlify-cli utility so that we can run the serverless function locally.
npm install netlify-cli -g
Now we will install two important libraries, axios and dotenv. axios will be used for making the HTTP calls and dotenv will help to load the FAUNA_SERVER_SECRET
environment variable from the .env
file into process.env
.
yarn add axios dotenv
Or:
npm i axios dotenv
Create serverless functions
Create a folder with the name, functions
at the root of the project folder. We are going to keep all serverless functions under it.
Now create a subfolder called utils
under the functions
folder. Create a file called query.js
under the utils
folder. We will need some common code to query the data store for all the serverless functions. The common code will be in the query.js
file.
First we import the axios library functionality and load the .env
file. Next, we export an async function that takes the query and variables. Inside the async function, we make calls using axios with the secret key. Finally, we return the response.
// query.js
const axios = require("axios");
require("dotenv").config();
module.exports = async (query, variables) => {
const result = await axios({
url: "https://graphql.fauna.com/graphql",
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FAUNA_SERVER_SECRET}`
},
data: {
query,
variables
}
});
return result.data;
};
Create a file with the name, get-shopnotes.js
under the functions
folder. We will perform a query to fetch all the shop notes.
// get-shopnotes.js
const query = require("./utils/query");
const GET_SHOPNOTES = `
query {
allShopNotes {
data {
_id
name
description
updatedAt
items {
data {
_id,
name,
checked,
urgent
}
}
}
}
}
`;
exports.handler = async () => {
const { data, errors } = await query(GET_SHOPNOTES);
if (errors) {
return {
statusCode: 500,
body: JSON.stringify(errors)
};
}
return {
statusCode: 200,
body: JSON.stringify({ shopnotes: data.allShopNotes.data })
};
};
Time to test the serverless function like an API. We need to do a one time setup here. Open a command prompt at the root of the project folder and type:
netlify login
This will open a browser tab and ask you to login and authorize access to your Netlify account. Please click on the Authorize button.
Next, create a file called, netlify.toml
at the root of your project folder and add this content to it,
[build]
functions = "functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
This is to tell Netlify about the location of the functions we have written so that it is known at the build time.
Netlify automatically provides the APIs for the functions. The URL to access the API is in this form, /.netlify/functions/get-shopnotes
which may not be very user-friendly. We have written a redirect to make it like, /api/get-shopnotes
.
Ok, we are done. Now in command prompt type,
netlify dev
By default the app will run on localhost:8888 to access the serverless function as an API.
Open a browser tab and try this URL, http://localhost:8888/api/get-shopnotes
:
Congratulations!!! You have got your first serverless function up and running.
Let us now write the next serverless function to create a ShopNote
. This is going to be simple. Create a file named, create-shopnote.js
under the functions
folder. We need to write a mutation by passing the required parameters.
//create-shopnote.js
const query = require("./utils/query");
const CREATE_SHOPNOTE = `
mutation($name: String!, $description: String!, $updatedAt: Time!, $items: ShopNoteItemsRelation!) {
createShopNote(data: {name: $name, description: $description, updatedAt: $updatedAt, items: $items}) {
_id
name
description
updatedAt
items {
data {
name,
checked,
urgent
}
}
}
}
`;
exports.handler = async event => {
const { name, items } = JSON.parse(event.body);
const { data, errors } = await query(
CREATE_SHOPNOTE, { name, items });
if (errors) {
return {
statusCode: 500,
body: JSON.stringify(errors)
};
}
return {
statusCode: 200,
body: JSON.stringify({ shopnote: data.createShopNote })
};
};
Please give your attention to the parameter, ShopNotesItemRelation
. As we had created a relation between the ShopNote
and Item
in our schema, we need to maintain that while writing the query as well.
We have de-structured the payload to get the required information from the payload. Once we got those, we just called the query method to create a ShopNote
.
Alright, let’s test it out. You can use postman or any other tools of your choice to test it like an API. Here is the screenshot from postman.
Great, we can create a ShopNote
with all the items we want to buy from a shopping mart. What if we want to add an item to an existing ShopNote
? Let us create an API for it. With the knowledge we have so far, it is going to be really quick.
Remember, ShopNote
and Item
are related? So to create an item, we have to mandatorily tell which ShopNote
it is going to be part of. Here is our next serverless function to add an item to an existing ShopNote
.
//add-item.js
const query = require("./utils/query");
const ADD_ITEM = `
mutation($name: String!, $urgent: Boolean!, $checked: Boolean!, $note: ItemNoteRelation!) {
createItem(data: {name: $name, urgent: $urgent, checked: $checked, note: $note}) {
_id
name
urgent
checked
note {
name
}
}
}
`;
exports.handler = async event => {
const { name, urgent, checked, note} = JSON.parse(event.body);
const { data, errors } = await query(
ADD_ITEM, { name, urgent, checked, note });
if (errors) {
return {
statusCode: 500,
body: JSON.stringify(errors)
};
}
return {
statusCode: 200,
body: JSON.stringify({ item: data.createItem })
};
};
We are passing the item properties like, name, if it is urgent, the check value and the note the items should be part of. Let’s see how this API can be called using postman,
As you see, we are passing the id
of the note while creating an item for it.
We won’t bother writing the rest of the API capabilities in this article, like updating, deleting a shop note, updating, deleting items, etc. In case, you are interested, you can look into those functions from the GitHub Repository.
However, after creating the rest of the API, you should have a directory structure like this,
We have successfully created a data store with Fauna, set it up for use, created an API backed by serverless functions, using Netlify Functions, and tested those functions/routes.
Congratulations, you did it. Next, let us build some user interfaces to show the shop notes and add items to it. To do that, we will use Gatsby.js (aka, Gatsby) which is a super cool, React-based static site generator.
The following section requires you to have basic knowledge of ReactJS. If you are new to it, you can learn it from here. If you are familiar with any other user interface technologies like, Angular, Vue, etc feel free to skip the next section and build your own using the APIs explained so far.
Set up the User Interfaces using Gatsby
We can set up a Gatsby project either using the starter projects or initialize it manually. We will build things from scratch to understand it better.
Install gatsby-cli globally.
npm install -g gatsby-cli
Install gatsby, react and react-dom
yarn add gatsby react react-dom
Edit the scripts section of the package.json
file to add a script for develop
.
"scripts": {
"develop": "gatsby develop"
}
Gatsby projects need a special configuration file called, gatsby-config.js
. Please create a file named, gatsby-config.js
at the root of the project folder with the following content,
module.exports = {
// keep it empty
}
Let’s create our first page with Gatsby. Create a folder named, src
at the root of the project folder. Create a subfolder named pages
under src
. Create a file named, index.js
under src/pages
with the following content:
import React, { useEffect, useState } from 'react';
export default () => {
const [loading, setLoading ] = useState(false);
const [shopnotes, setShopnotes] = useState(null);
return (
<>
<h1>Shopnotes to load here...</h1>
</>
)
}
Let’s run it. We generally need to use the command gatsby
develop to run the app locally. As we have to run the client side application with netlify functions, we will continue to use, netlify dev
command.
netlify dev
That’s all. Try accessing the page at http://localhost:8888
. You should see something like this,
Gatsby project build creates a couple of output folders which you may not want to push to the source code repository. Let us add a few entries to the .gitignore
file so that we do not get unwanted noise.
Add .cache
, node_modules
and public
to the .gitignore
file. Here is the full content of the file:
.cache
public
node_modules
*.env
At this stage, your project directory structure should match with the following:
Thinking of the UI components
We will create small logical components to achieve the ShopNote
user interface. The components are:
- Header: A header component consists of the Logo, heading and the create button to create a shopnote.
- Shopenotes: This component will contain the list of the shop note (
Note
component). - Note: This is individual notes. Each of the notes will contain one or more items.
- Item: Each of the items. It consists of the item name and actions to add, remove, edit an item.
You can see the sections marked in the picture below:
Install a few more dependencies
We will install a few more dependencies required for the user interfaces to be functional and look better. Open a command prompt at the root of the project folder and install these dependencies,
yarn add bootstrap lodash moment react-bootstrap react-feather shortid
Lets load all the Shop Notes
We will use the Reactjs useEffect
hook to make the API call and update the shopnotes state variables. Here is the code to fetch all the shop notes.
useEffect(() => {
axios("/api/get-shopnotes").then(result => {
if (result.status !== 200) {
console.error("Error loading shopnotes");
console.error(result);
return;
}
setShopnotes(result.data.shopnotes);
setLoading(true);
});
}, [loading]);
Finally, let us change the return section to use the shopnotes data. Here we are checking if the data is loaded. If so, render the Shopnotes
component by passing the data we have received using the API.
return (
<div className="main">
<Header />
{
loading ? <Shopnotes data = { shopnotes } /> : <h1>Loading...</h1>
}
</div>
);
You can find the entire index.js file code from here The index.js
file creates the initial route(/
) for the user interface. It uses other components like, Shopnotes
, Note
and Item
to make the UI fully operational. We will not go to a great length to understand each of these UI components. You can create a folder called components under the src
folder and copy the component files from here.
Finally, the index.css
file
Now we just need a css file to make things look better. Create a file called index.css
under the pages
folder. Copy the content from this CSS file to the index.css
file.
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css'
That’s all. We are done. You should have the app up and running with all the shop notes created so far. We are not getting into the explanation of each of the actions on items and notes here not to make the article very lengthy. You can find all the code in the GitHub repo. At this stage, the directory structure may look like this,
A small exercise
I have not included the Create Note UI implementation in the GitHib repo. However, we have created the API already. How about you build the front end to add a shopnote? I suggest implementing a button in the header, which when clicked, creates a shopnote using the API we’ve already defined. Give it a try!
Let’s Deploy
All good so far. But there is one issue. We are running the app locally. While productive, it’s not ideal for the public to access. Let’s fix that with a few simple steps.
Make sure to commit all the code changes to the Git repository, say, shopnote. You have an account with Netlify already. Please login and click on the button, New site from Git.
Next, select the relevant Git services where your project source code is pushed. In my case, it is GitHub.
Browse the project and select it.
Provide the configuration details like the build command, publish directory as shown in the image below. Then click on the button to provide advanced configuration information. In this case, we will pass the FAUNA_SERVER_SECRET
key value pair from the .env file. Please copy paste in the respective fields. Click on deploy.
You should see the build successful in a couple of minutes and the site will be live right after that.
In Summary
To summarize:
- The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
- 70% – 80% of the features that once required a custom back-end can now be done either on the front end or there are APIs, services to take advantage of.
- Fauna provides the data API for the client-serverless applications. We can use GraphQL or Fauna’s FQL to talk to the store.
- Netlify serverless functions can be easily integrated with Fauna using the GraphQL mutations and queries. This approach may be useful when you have the need of custom authentication built with Netlify functions and a flexible solution like Auth0.
- Gatsby and other static site generators are great contributors to the Jamstack to give a fast end user experience.
Thank you for reading this far! Let’s connect. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow.
The post How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna appeared first on CSS-Tricks.
You can support CSS-Tricks by being an MVP Supporter.
source https://css-tricks.com/how-to-create-a-client-serverless-jamstack-app-using-netlify-gatsby-and-fauna/
No comments:
Post a Comment