If you find this useful, please consider sharing it on social media to help spread the word!

Setup

This article assumes you already have NodeJS, NPM, and MongoDB installed on your workstation.

Lets just jump right into this, pop open a command line shell and run npm install --global gatsby-cli , followed by gatsby new subscription-example , that will get you a basic Gatsby starter app all ready to go.

You can at this point open up the project folder in your favorite text editor (which is definitely VSCodium, right?), bring up the in-editor terminal, and give your knuckles a good crack (or don’t? They’re your hands after all). You are now ready to proceed to the next step.

Speaking of subscription forms, you can subscribe to my blog at the bottom of this page 🤓

Apollo Server

We are going to set up an Apollo server on ExpressJS, and in the end we will serve the Gatsby frontend from the same Express server for a seamless single-host setup.

Let’s first install some dependancies:

npm install --save graphql react-apollo apollo-server apollo-boost apollo-server-express isomorphic-fetch express mongoose morgan cors dotenv esm typescript

What was all that for?

graphql Apollo uses GraphQL to resolve queries.

react-apollo , apollo-server , apollo-boost , apollo-server-express , isomorphic-fetch These are all for Apollo communication between the frontend and backend.

, , , , express , mongoose , morgan Express will be our server, Mongoose will link the server up to our MongoDB database, and Morgan is the standard traffic logger for Express.

, , cors CORS lets us control what URL’s are allowed to make Apollo queries.

dotenv Loads variables from the .env file in the root directory of the project.

esm Lets us use ES6 syntax in plain NodeJS files.

typescript Just a dependancy that needs to be manually installed



Great, now let’s make a folder in the root of the project called server , we are going to put the following 5 files in the server folder. I have added extra comments in the code to make it more readable, but feel free to ask any questions in the comments section at the bottom of this page if something isn’t totally clear.

This is where we will setup the connection to MongoDB.

import mongoose from 'mongoose' const dbURL = process . env . DATABASE_URL || 'mongodb://localhost:27017/subscriptionexample' mongoose . connect ( dbURL ) . catch ( ( err ) => { console . log ( err ) } ) const db = mongoose . connection db . on ( 'error' , ( ) => { setTimeout ( ( ) => { mongoose . connect ( dbURL ) . catch ( ( err ) => { console . log ( err ) } ) } , 10000 ) } ) export default db

Here we define a model of the data types we will be using, you can modify these as needed, just remember what to change for the later steps so they match.

Note: This is a very basic example, you would do well to add a scalar for the Email type to ensure you only get Email addresses instead of just any string.

import { gql } from 'apollo-server-express' const schema = gql ` # At the time of writing, there is a bug that will crash # the application if there isn't at least one query # defined in the schema, so we have a dummy query. type Query { dummy: String } # We will use the signUp mutation to add data to the database, # we want to take in some user information as strings, # and reply with a response. type Mutation { signUp( firstName: String! lastName: String! email: String! ): Response } # The response will also be a string. type Response { result: String } ` export default schema

In this file, we will make sure the subscriber is new and not already in the database, attempt to add them to the database, and return a response for Apollo to send back the the client upon completion.

import db from './database' const subscribersCollection = 'subscribers' export const addSubscriber = async ( args ) => { if ( await checkForDuplicateEmail ( subscribersCollection , args ) ) { return 'This Email address is already subscribed!' } else { return dbInsertSubscriber ( subscribersCollection , args ) } } const checkForDuplicateEmail = ( collectionName , { email } = args ) => { return new Promise ( async ( resolve ) => { if ( await checkCollectionExists ( collectionName ) ) { db . collection ( collectionName ) . findOne ( { email } , ( err , data ) => { if ( err ) console . log ( err ) if ( data ) { resolve ( true ) } else { resolve ( false ) } } , ) } else { resolve ( false ) } } ) } const checkCollectionExists = ( collectionName ) => { return new Promise ( ( resolve ) => { db . db . listCollections ( { name : collectionName } ) . next ( ( err , collectionInfo ) => { if ( err ) console . log ( err ) if ( collectionInfo ) { resolve ( true ) } else { resolve ( false ) } } ) } ) } const dbInsertSubscriber = ( collectionName , args ) => { return new Promise ( ( resolve ) => { db . collection ( collectionName ) . insertOne ( { ... args , subscriptionDate : new Date ( ) , cancelDate : null , active : true , } , ( err ) => { if ( err ) { console . log ( err ) resolve ( 'Error: Request rejected - pleasae try again later...' , ) } else { resolve ( 'Your subscription has been submitted, thank you!' ) } } , ) } ) }

This is where we define how requests are handled or ‘resolved’.

import db from './database' import { addSubscriber } from './dbActions' const resolvers = { Mutation : { signUp : async ( parent , args ) => { if ( db . readyState !== 1 ) { return { result : 'Error: Database unreachable - please try again later...' , } } else { const result = await addSubscriber ( args ) return { result } } } , } , } export default resolvers

Here we will tie all of that together.

import cors from 'cors' import express from 'express' import schema from './schema' import resolvers from './resolvers' import { ApolloServer } from 'apollo-server-express' const morgan = require ( 'morgan' ) require ( 'dotenv' ) . config ( ) const GQL_URL = process . env . GQL_URL || '/api/v1' const SERVE_PORT = process . env . GQL_PORT || 3000 const CORS_ADDRESS = process . env . CORS_ADDRESS || '*' const app = express ( ) app . use ( cors ( { origin : CORS_ADDRESS , } ) , ) app . use ( morgan ( 'tiny' ) ) const server = new ApolloServer ( { typeDefs : schema , resolvers , } ) app . use ( express . static ( 'public' ) ) server . applyMiddleware ( { app , path : GQL_URL } ) app . get ( '*' , function ( req , res ) { res . redirect ( '/404' ) } ) app . listen ( { port : SERVE_PORT } , ( ) => { console . log ( 'Server running on http://localhost:' + SERVE_PORT ) } )

At this point, you can run node -r esm server/index from the terminal, and have a working Apollo server. Pointing your browser to http://localhost:3000 will give you Cannot GET / , that’s fine for now as we haven’t build the client side. But if you visit http://localhost/api/vi , you will be greeted with the GraphQL Playground.

Try it out by entering this on the left side of the window and hitting the play button:

mutation { signUp ( firstName : "Some" lastName : "Body" email : "SomeBody@SomeWhere.tld" ) { result } }

You should see the result on the right side Your subscription has been submitted, thank you! , and if you hit play again you should see This Email address is already subscribed! , you will also notice that this data has been inserted into your MongoDB database, just as we had planned! Now lets make that work with Gatsby.

Further Reading: That's all for our Apollo/GraphQL server, if you want to take a deep dive into GraphQL I recommend the book Learning GraphQL: Declarative Data Fetching for Modern Web Apps by O'Reilly Media. Available on Amazon.

Gatsby Client

First lets create a new folder apollo within the src directory, and within that a new file called client.js , here we will define the connection to the Apollo server we just built.

import ApolloClient from 'apollo-boost' import fetch from 'isomorphic-fetch' const API_URI = process . env . GATSBY_API_URI || 'http://localhost:3000/api/v1' export const client = new ApolloClient ( { uri : API_URI , fetch , } )

gatsby-ssr.js and gatsby-browser.js

In order to use Apollo in Gatsby, we also need to wrap the whole app in the Apollo provider. We do this in both gatsby-ssr.js and gatsby-browser.js , using the same exact code:

import React from 'react' import { ApolloProvider } from 'react-apollo' import { client } from './src/apollo/client' export const wrapRootElement = ( { element } ) => ( < ApolloProvider client = { client } > { element } </ ApolloProvider > )

We also need to source our .env file on the client side, so put this at the very top of gatsby-config.js :

require ( 'dotenv' ) . config ( { path : ` .env ` , } )

This goes at the top, you don’t need to change the rest of this file.

Now lets tie it all together with a sign up page:

Note: Again, this is a basic example, there is no form validation here but it would be wise to implement.

import React , { useState } from 'react' import { Link } from 'gatsby' import { Mutation } from 'react-apollo' import gql from 'graphql-tag' import Layout from '../components/layout' import SEO from '../components/seo' const SUBSCRIBE_MUTATION = gql ` mutation SignUp( $firstName: String! $lastName: String! $email: String! ) { signUp( firstName: $firstName lastName: $lastName email: $email ) { result } } ` const SubscriptionPage = ( ) => { const [ firstNameValue , setFirstNameValue ] = useState ( '' ) const [ lastNameValue , setLastNameValue ] = useState ( '' ) const [ emailValue , setEmailValue ] = useState ( '' ) return ( < Layout > < SEO title = ' Subscription Page ' /> < h1 > Subscribe to the Mailing List </ h1 > < p > Please fill out the form below: </ p > { } < Mutation mutation = { SUBSCRIBE_MUTATION } > { } { ( signUp , { loading , error , data } ) => ( < React.Fragment > <form onSubmit= { async ( event ) => { event . preventDefault ( ) signUp ( { variables : { firstName : firstNameValue , lastName : lastNameValue , email : emailValue , } , } ) } } > { } < div style = { { padding : '20px' } } > < label htmlFor = ' firstNameInput ' > First Name: </ label > < input id = ' firstNameInput ' value = { firstNameValue } onChange = { ( event ) => { setFirstNameValue ( event . target . value ) } } /> </ div > < div style = { { padding : '20px' } } > < label htmlFor = ' lastNameInput ' > Last Name: </ label > < input id = ' lastNameInput ' value = { lastNameValue } onChange = { ( event ) => { setLastNameValue ( event . target . value ) } } /> </ div > < div style = { { padding : '20px' } } > < label htmlFor = ' emailInput ' > Email Address: </ label > < input id = ' emailInput ' value = { emailValue } onChange = { ( event ) => { setEmailValue ( event . target . value ) } } /> </ div > < div style = { { padding : '20px' } } > < button type = ' submit ' > Subscribe! </ button > </ div > </ form > { } < div style = { { padding : '20px' } } > { loading && < p > Loading... </ p > } { error && ( < p > An unknown error has occured, please try again later... </ p > ) } { data && < p > { data . signUp . result } </ p > } </ div > </ React.Fragment > ) } </ Mutation > < Link to = ' / ' > Go back to the homepage </ Link > </ Layout > ) } export default SubscriptionPage

Just to be thorough, let’s add a link to the subscribe page from the index page, here is the whole file:

import React from 'react' import { Link } from 'gatsby' import Layout from '../components/layout' import Image from '../components/image' import SEO from '../components/seo' const IndexPage = ( ) => ( < Layout > < SEO title = ' Home ' /> < h1 > Hi people </ h1 > < p > < Link to = ' /subscribe/ ' > Subscribe to our Mailing List! </ Link > </ p > < p > Welcome to your new Gatsby site. </ p > < p > Now go build something great. </ p > < div style = { { maxWidth : ` 300px ` , marginBottom : ` 1.45rem ` } } > < Image /> </ div > < Link to = ' /page-2/ ' > Go to page 2 </ Link > </ Layout > ) export default IndexPage

Lets run npm install --global concurrently nodemon , concurrently will allow us to run multiple commands (server and client) from the same NPM script, nodemon as you probably already know will listen for file changes and restart the running process automatically so you don’t have to.

In the package.json file, edit the develop line under scripts so it looks like this:

"develop" : "concurrently \"nodemon -r esm server/index.js\" \"gatsby develop\"" ,

Now run npm run develop from the command line, and point your browser to http://localhost:8000/subscribe . Punch in a name and Email and hit the subscribe button, you should see Your subscription has been submitted, thank you! pop up below the form, and you should also have the corresponding data in your MongoDB Database.

Wrap Up

All that’s left to do wrap it up with a nice little bow on top for deployment. Remember all those .env checks we used? This is where we need them, make a new file in the root of the project called, you guessed it, .env .

NODE_ENV = production GQL_URL = / api / v1 GATSBY_API_URI = http : / / localhost : 3000 / api / v1 SERVE_PORT = 3000 CORS_ADDRESS = http : / / localhost : 3000 DATABASE_URL = mongodb : / / localhost : 27017 / subscriptionexample

You will obviously use different values when actually deploying, but these will work for our local example.

In the package.json file, edit the serve line under scripts , to look like this:

"serve" : "nodemon -r esm server/index.js" ,

We will only run the express server in production since it can serve both the Apollo API and the static Gatsby build that makes up the client side. Run npm run build and then npm run serve from the command line. Now you can visit http://localhost:3000/subscribe/ (note the port change) to fill out and test your form, you will also note that the requests to the Gatsby frontend are logged in the console in addition to the Apollo API request.

Note: If you are working on multiple projects that serve a Gatsby frontend via Express static, you may find that when switching between them on localhost causes the index page to not load due to MIME-Type issues, the solution is to DELETE the public folder and re-run npm run build . If you switch back and forth a lot it’s probably easier just to run them on different ports.

Bonus Points

I know what you’re thinking, “What about Docker!?”, well I’ve got you covered. You’ll need to set up a MongoDB instance on your Docker host in a user-defined bridge network, and give it a static IP address.

Dockerfile

Make a new Dockerfile at the root of the project folder:

FROM node : 10 - alpine ENV NODE_ENV = production ENV GQL_URL = / api / v1 ENV GATSBY_API_URI = / api / v1 ENV SERVE_PORT = 3000 ENV CORS_ADDRESS = https : / / yourdomain . com ENV DATABASE_URL = mongodb : / / 172.16 .32 .7 : 27017 / subscribers COPY . / / opt / app RUN apk add -- no - cache -- virtual . gyp python make g ++ RUN npm install -- global gatsby - cli WORKDIR / opt / app RUN npm install RUN npm run build EXPOSE 3000 / tcp CMD npm run serve

Note: You’ll need to change the CORS_ADDRESS and DATABASE_URI env declarations to fit your scenario

There you have it, deploy away!