DEV Community

Agney Menon
Agney Menon

Posted on • Originally published at blog.agney.dev

Creating a JAMStack Reader App with React & Netlify Functions

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:

[build]
  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.

const proxy = require("http-proxy-middleware");

module.exports = function(app) {
  app.use(
    proxy("/.netlify/functions/", {
      target: "http://localhost:9000/",
      pathRewrite: {
        "^/\\.netlify/functions": "",
      },
    })
  );
};

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.

import React from "react";

const App = () => {
  const handleSubmit = () => {};
  return (
    <main>
      <form onSubmit={handleSubmit}>
        <input type="url" placeholder="Enter url here" name="url" label="url" />
        <button>View</button>
      </form>
    </main>
  )
}

Filling in the handleSubmit function:

import { stringify } from "qs";  // for encoding the URL as a GET parameter

const handleSubmit = (event) => {
  event.preventDefault();
  const url = event.target.url.value;
  fetch(
    `/.netlify/functions/parse?${stringify({ q: reqUrl })}`
  ).then(response => response.json())
}

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:

import Mercury from "@postlight/mercury-parser";

export async function handler(event) {
  const parameters = event.queryStringParameters;
  const url = parameters.q;

  if (!url) {
    return {
      statusCode: 400,
      body: JSON.stringify({ error: "Invalid/No URL provided" }),
    };
  }
  try {
    const response = await Mercury.parse(url);
    return {
      statusCode: 200,
      body: JSON.stringify({ data: response }),
    };
  } catch (err) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: err }),
    };
  }
}

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:

import React, { useState } from "react";
import { stringify } from "qs";

const App = () => {
  const [ result, setResult ] = useState(null);
  const handleSubmit = (event) => {
    event.preventDefault();
    const url = event.target.url.value;
    fetch(
      `/.netlify/functions/parse?${stringify({ q: reqUrl })}`
    )
      .then(response => response.json())
      .then(jsonResponse => setResult(jsonResponse.data));
  }
  return (
    <main>
      <form onSubmit={handleSubmit}>
        <input type="url" placeholder="Enter url here" name="url" label="url" />
        <button>View</button>
      </form>
      {result && (
        <article dangerouslySetInnerHTML={{ __html: data.content }} />
      )}
    </main>
  )
}

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.

Bonus, you can set the app as a share target to share URLs directly into your PWA.

You can find the complete code in the following repository:

GitHub logo agneym / the-reader

A JAM Stack Reading Mode PWA with React & Netlify Functions

Application Demo

Originally written on my blog

Top comments (0)