Creating a JAMStack Reader App with React & Netlify Functions

Posted on January 04, 2020

I’m in 💖 with the JAMStack, It gets the work done. One of the very exciting companies in this area is Netlify. Anyone who tested their hosting would tell you it’s top class and I would recommend it anyday.

In this post we will explore using their Serverless functions with create-react-app.

The What.

What we intend to create a reading application. You give it the URL and allows you to view the simplified content fit for reading.

The How

We ideally to parse the url string from the backend to avoid getting blocked by CORS. We will use a Netlify Function to achieve this. We will use Postlight’s Mercury Parser with the function to parse the simplified version from URL.

The Detail

First let’s create a new React application with create-react-app:

npm init react-app the-reader

The Build step

Now, to setup Netlify functions, create a top level folder, I’m naming it functions. We have to update the build step so that the function is also build when we run yarn build.

Netlify has published a package netlify-lambda to help with the build:

yarn add netlify-lambda npm-run-all --dev

npm-run-all is used to run both tasks in parallel. In package.json:

"scripts": {
"build": "run-p build:**",
"build:app": "react-scripts build",
"build:lambda": "netlify-lambda build functions/",

Create a netlify.toml so that netlify knows where the build is:

command = "yarn build"
functions = "build-lambda" # netlify-lambda gets build to this folder
publish = "build" # create-react-app builds to this folder

Remember to add build-lambda to .gitignore.

Create your first function by creating a JS file in functions folder we created earlier.

Netlify can recognise JS files in the folder or in nested folders.

In functions/parse.js:

export async function handler(event) {
return {
statusCode: 200,
body: JSON.stringify({ data: 'hello world' }),

Dummy function

From the frontend application you can now use fetch to query .netlify/functions/parse.js (your folder structure prepended with .netlify/) to get the dummy response we put in. But with a twist, it works only when you deploy the application to Netlify. That’s not a good development methodology. This is because the functions are not build yet and there is .netlify/ path to get the data from.

netlify-lambda has a serve mode for development, so that the functions can be build for any changes and updated to a server.

Add the following to package.json and keep it running in the background with npm start:

"scripts": {
"serve:lambda": "netlify-lambda serve functions/",

The Proxy

You will find that the functions is now running on a server with localhost:9000. But even if you could add an environment variable to query this server, there is an issue with CORS now. Your frontend and functions are running on different servers. To get around this, you can add a proxy with create-react-app. You can find complete instructions in the docs.

What we have to do is to add src/setupProxy.js, you don’t have to import this file anywhere, just create, add code and ✨ restart your development server.

1const proxy = require('http-proxy-middleware');
3module.exports = function(app) {
4 app.use(
5 proxy('/.netlify/functions/', {
6 target: 'http://localhost:9000/',
7 pathRewrite: {
8 '^/\\.netlify/functions': '',
9 },
10 })
11 );

What this is essentially doing is to rewrite any API calls to .netlify/functions to localhost:9000 and get response from there. This only works in development, so it works without the server in production.

The API call

First, let’s setup a form where user can enter a URL and request the server.

1import React from 'react';
3const App = () => {
4 const handleSubmit = () => {};
5 return (
6 <main>
7 <form onSubmit={handleSubmit}>
8 <input type="url" placeholder="Enter url here" name="url" label="url" />
9 <button>View</button>
10 </form>
11 </main>
12 );

Filling in the handleSubmit function:

1import { stringify } from 'qs'; // for encoding the URL as a GET parameter
3const handleSubmit = event => {
4 event.preventDefault();
5 const url =;
6 fetch(`/.netlify/functions/parse?${stringify({ q: reqUrl })}`).then(
7 response => response.json()
8 );

If you run this function now, it will return the { data: "Hello world" } we added earlier (hopefully).

To return some real data, let’s modify the functions/parse.js to:

1import Mercury from '@postlight/mercury-parser';
3export async function handler(event) {
4 const parameters = event.queryStringParameters;
5 const url = parameters.q;
7 if (!url) {
8 return {
9 statusCode: 400,
10 body: JSON.stringify({ error: 'Invalid/No URL provided' }),
11 };
12 }
13 try {
14 const response = await Mercury.parse(url);
15 return {
16 statusCode: 200,
17 body: JSON.stringify({ data: response }),
18 };
19 } catch (err) {
20 return {
21 statusCode: 500,
22 body: JSON.stringify({ error: err }),
23 };
24 }

The function takes URL as an argument through queryStringParameters and uses Mercury.parse to get the simplified version and return it to the user.

Now, running the frontend would get you the real response from the serverless function (which underwhelmingly has a server now, but you can always push and get it deployed).

Some changes on the frontend to display the data from backend:

1import React, { useState } from 'react';
2import { stringify } from 'qs';
4const App = () => {
5 const [result, setResult] = useState(null);
6 const handleSubmit = event => {
7 event.preventDefault();
8 const url =;
9 fetch(`/.netlify/functions/parse?${stringify({ q: reqUrl })}`)
10 .then(response => response.json())
11 .then(jsonResponse => setResult(;
12 };
13 return (
14 <main>
15 <form onSubmit={handleSubmit}>
16 <input type="url" placeholder="Enter url here" name="url" label="url" />
17 <button>View</button>
18 </form>
19 {result && <article dangerouslySetInnerHTML={{ __html: data.content }} />}
20 </main>
21 );

and we are Done 🥂.

To convert this to a PWA, you can very simply add the service workers on the create-react-app and adjust the parameters in manifest.json.

Share Target

It’s too much of work to actually copy the URL and copy paste into our app, especially on mobile. But for PWAs what we can do is to register the application as a share target. You can do this by specifying the following in manifest.json:

"share_target": {
"action": "/",
"method": "GET",
"params": {
"text": "q"

The important thing here is the params object. On the left is what is to be passed, there are different params like title, text and url. For some wierd reason, Chrome sends the URL of the page in the text parameter and that is what we are substituting here.

When something is shared to the application, the URL would be:

To process this, the frontend can:

1const location = useLocation();
3useEffect(() => {
4 const searchParams = new URLSearchParams(;
5 const url = searchParams.get('q');
6 if (url) {
7 handleView(url);
8 }
9}, []);

You can find the complete code in the repository

Application Demo


Buy me a coffeeBuy me a coffee