How to use Serverless Functions with SSR
- Gatsby
- Gatsby Serverless Functions
- React
- JavaScript
- Tailwind
- Google Analytics
In this post I’ll explain how to abstract and reuse the same function logic across all sides of the Gatsby stack; Serverless Functions, Server-side Rendering (SSR) and Static Site Generation (SSG).
I’ve created a demo which demonstrates what can be achieved using this approach. The repository is Open-source and contains x2 branches.
The main branch contains all the code used in the demo site and there’s a minimal example branch with just the bits that make shared functions possible.
Gatsby Shared Functions
How Not To Do It
When I first needed to use function logic in both a Serverless Function and an SSR page, I did something like this.
// src/api/some-endpoint.js
export default function handler(req, res) {
res.status(200).json({
message: 'A ok!',
data: [
{ date: '24/09/2022', value: '122' },
{ date: '25/09/2022', value: '117' },
{ date: '26/09/2022', value: '52' },
],
});
}
// src/pages/index.js
import React, { useState, useEffect } from 'react';
const Page = ({ serverData }) => {
const [clientResults, setClientResults] = useState();
const getClientData = async () => {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
setClientResults(results.data);
};
useEffect(() => {
getClientData();
}, []);
return (
<div>
<pre>{JSON.stringify(clientResults, null, 2)}</pre>
<pre>{JSON.stringify(serverData.serverResults, null, 2)}</pre>
</div>
);
};
export async function getServerData() {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
return {
props: {
serverResults: results.data,
},
};
}
export default Page;
This Won’t Work!
The reason, this won’t work is because the Serverless Function lives on the server, and getServerData
also lives on
the server. In short this approach is attempting to call a server side function from the server.
Let me explain…
getServerData
is run on the server so it can’t really make an HTTP request to the Serverless Function because the
Serverless Function is also on the server. To quote Dustin Schau VP of Engineering at
Gatsby.
It creates a kind of back-and-forth that could introduce undue latency
How To Do It
In a word, abstraction. Since both the Serverless Function and getServerData
live on the server they can import
(require), and use the same function.
Shared Function
You can save a shared function anywhere in your Gatsby project. I’ve chosen to save the file in src/utils
. Below is
just a simple function that returns an array of dates and values. In the demo site I’m making a request to the Google
Analytics API and returning actual page view data for paulie.dev.
Your data may well be different, but the principle is the same.
// src/utils/shared-function.js
module.exports = function () {
return {
data: [
{ date: '24/09/2022', value: '122' },
{ date: '25/09/2022', value: '117' },
{ date: '26/09/2022', value: '52' },
],
};
};
For reference, here’s the shared-function used in the demo site.
Serverless
This time around the Serverless Function imports / requires the shared function and awaits the response before returning it.
// src/api/some-endpoint
const util = require('../utils/shared-function');
export default async function handler(req, res) {
const response = await util();
res.status(200).json({ message: 'A ok!', data: response.data });
}
SSR
It’s a similar story for the getServerData
function. Import / require the shared function and await the response
before returning it to the page.
// src/pages/index.js
export async function getServerData() {
const util = require('../utils/shared-function');
const response = await util();
return {
props: {
serverResults: response.data,
},
};
}
This Will Work!
This will work because both the Serverless Function and the getServerData
function are responsible for their own
“request”. Each of their response’s is returned in a way that’s suitable for their application. When successful, the
data can be accessed by the page by either referencing a prop
, or a useState
value.
The main difference with this approach is, it’s DRY. The actual “business logic” of what the shared function is doing is reused by both methods. Any changes made to the shared function will be reflected in both the Serverless Function and SSR response.
SSG Page
Yep, as it always has been with Gatsby you can source data from any CMS, API or Database and store the response in Gatsby’s Data Layer ready to be queried using GraphQL.
sourceNodes
For SSG you can import / require the shared function, await the response and then pump the data into Gatsby’s Data Layer
using createNode
.
// gatsby-node.js
const util = require('./src/utils/shared-function');
exports.sourceNodes = async ({
actions,
createNodeId,
createContentDigest,
}) => {
const response = await util();
response.data.forEach((item) => {
actions.createNode({
...item,
id: createNodeId(item.date),
internal: {
type: 'staticResults',
contentDigest: createContentDigest(item),
},
});
});
};
All Together
Back to src/pages/index.js
where it’s now possible to query this data using GraphQL and return it to the page using
the data
prop.
// src/pages/index.js
import React, { useState, useEffect } from 'react';
+ import { graphql } from 'gatsby';
+ const Page = ({ data, serverData }) => {
- const Page = ({ serverData }) => {
const [clientResults, setClientResults] = useState();
const getClientData = async () => {
const response = await fetch('/api/some-endpoint');
const results = await response.json();
setClientResults(results.data);
};
useEffect(() => {
getClientData();
}, []);
return (
<div>
<pre>{JSON.stringify(clientResults, null, 2)}</pre>
<pre>{JSON.stringify(serverData.serverResults, null, 2)}</pre>
+ <pre>{JSON.stringify(data.allStaticQuery.nodes, null, 2)}</pre>
</div>
);
};
+ export const query = graphql`
+ query {
+ allStaticResults {
+ nodes {
+ value
+ date
+ }
+ }
+ }
+`;
export async function getServerData() {
const util = require('../utils/shared-function');
const response = await util();
return {
props: {
serverResults: response.data
}
};
}
export default Page;
The Demo
In the demo site, take a look at the Serverless Analytics chart, give the page a refresh. Notice it’s never blank.
This is because I first render the chart with data from serverData
. However, because I wanted to make this chart
interactive, I need to make a client side request to the Serverless Function which returns data specific to date ranges
set by the user.
Also… go ahead and disable JavaScript in your browser. Notice again, the charts are never blank!

These are “hand cranked” created using nothing more than SVG / SVG elements and good ol’ mathematics. You can see the
src
for the Line Chart here:
line-chart.js.
I chose to “roll my own charts” because:
- I like Data Viz.
- Some charting libraries only render the chart in the browser when JavaScript is enabled.
Since the data is available on both the client and the server, I want to make sure the charts can also be rendered on the Server. Server-side rendering SVG’s means they are statically generated as HTML elements and don’t require JavaScript to render.
The only slight snag is the tooltip. This doesn’t work if JavaScript is disabled. The tooltip displays more information about each of the plot marks which are kinda important. To overcome this I added a toggle switch which uses the checkbox hack to toggle dates and values in the ticks at the bottom of the chart.
I’ll be writing a little more about SVG charts soon so stay tuned!
Further Reading
- Create an SVG Doughnut Chart from scratch for your Gatsby blog
- Add data to Gatsby’s GraphQL layer using sourceNodes
- Become a Data Champion with Gatsby