Deploying a React app with lambda functions to Netlify
So, you've built your API. You've built your frontend app. But how do you get it online?
If you check my previous post, about building an Express backend to protect sensitive information consumed by your React app, you'll get an idea of why we can't simply deploy a data-dependent React app to Netlify (a platform for hosting static sites and single-page apps).
Moreover, Netlify does not have the capacity to accommodate a backend in the traditional sense. Instead, we have to create a custom, serverless one in the cloud β alternately known as Netlify Functions or AWS Lambda Functions, depending on the platform. (Just to clear up any confusion, Netlify's lambda functions are based on the AWS ones.)
What's a lambda function?
When I first heard people at work talking about a "lambda function" in this context, I was extremely confused. Wasn't a lambda function an inline, anonymous function within your code? What did that have to do with servers? I still think that AWS could have chosen a much less misleading name, but for the purposes of this post, especially because of the names of the packages used, from here I'll refer to them as lambda functions.
I like this definition:
AWS Lambda is a serverless compute service that runs your code in response to events and automatically manages the underlying compute resources for you. These events may include changes in state or an update, such as a user placing an item in a shopping cart on an ecommerce website.
It's a little bit hard to grasp how this is different to any normal function, but hopefully this will become clearer as the post goes on.
In this post, I will be going through how to set up a React app that calls an external API β these calls will be the lambda functions β to be deployed on Netlify.
Setting up your project
This post will assume you have your React project, and you already have some endpoints from an external API (say, from a Django REST API you built yourself). If you haven't already, please read previous post as this includes some of the packages you'll need to install.
Before we can even think about making our application lambda-ready, we need to make an account on Netlify and install its CLI β there's some handy documentation on that here. Then, install Netlify Lambda with npm install netlify-lambda
. You should also run git init
inside your project and link your GitHub repository to Netlify, as you will be deploying your project to Netlify using Git.
While in the root of your project, create the following:
- a new directory called
.netlify
, then inside it, a directory calledfunctions
- a new file called
netlify.toml
in the root.
Add the following information to netlify.toml
:
[build]
functions = "src/api"
publish = "build"
[[redirects]]
from = "/*"
to = "/.netlify/functions/:splat"
status = 200
As you can see, in this config file, we've specified that the lambda functions will be stored in src/api
. The build
directory, which is created when you run the build script in your project, indicates the directory that will be published β as with any other React project. The to =
part in the redirects section is where it gets interesting β this is the pattern of what our proxy URLs will be (the :splat
part is funny to me). More on that later.
Next, we need to navigate to the src
directory, create a file called setupProxy.js
, and put in the following, using this special middleware for Node:
const proxy = require("http-proxy-middleware");
module.exports = function(app) {
app.use(
proxy("/.netlify/functions/", {
target: "http://localhost:9000/",
pathRewrite: {
"^/\\.netlify/functions": "",
},
}),
);
};
This determines that the proxy server will be running on port 9000 and that the compiled functions, located in /.netlify/functions/
, will be accessed at the same path through the browser (fun bit of regex there in the pathRewrite
).
Finally, go to the scripts section of package.json
and add the following:
"scripts": {
...
"start:lambda": "netlify-lambda serve src/api",
"build:lambda": "netlify-lambda build src/api",
...
},
Creating lambda functions
So now we've done some setting up, it's time to write the actual lambda functions!
In the src
directory, create another directory called api
. In there, create a file called sample-function.js
(you can probably guess what will be in there).
exports.handler = async () => ({ statusCode: 200, body: "hello world" });
Now, run npm run start:lambda
in your terminal and visit http://localhost:9000/.netlify/functions/sample-function
in your browser. You should find a little "hello world" response there!
Okay, so now we know it works. Let's create some functions that actually call the API. Create a new file within src/api
called get-restaurant-list.js
and add the following:
const axios = require("axios");
require("dotenv").config();
exports.handler = async () => {
// initialise the response variable:
let response
try {
response = await axios.get(`${process.env.REACT_APP_PROD_API_URL}/restaurants`, {
headers: {
"Authorization": `JWT ${process.env.REACT_APP_PROD_JWT_TOKEN}`,
"Accept": "application/json",
"Content-Type": "application/json"
}
})
} catch (err) {
console.log(err)
return {
statusCode: err.statusCode,
body: JSON.stringify({
error: err.message
}),
// it's important that you add these headers in the error
// response for debugging purposes:
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Content-Type": "application/json"
},
}
}
return {
statusCode: 200,
body: JSON.stringify({
data: response.data
}),
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Content-Type": "application/json"
},
}
}
As you can see, it's a pretty typical Axios GET request. We've created a function with exports.handler
, made the request, and added error handling. When you visit http://localhost:9000/.netlify/functions/get-restaurant-list
in the browser, you should see the JSON response of all your restaurants.
In my previous post, I explained how you can create environment variables on React projects. You can use them here, too β just remember to add them to Netlify to when you've created your site there. You can also avoid a mistake that I was puzzling over for days by formatting the variables properly in the Netlify dashboard. In the local React project, they'll look like something like this:
REACT_APP_PROD_API_URL="https://some-api-url.com/v1/"
REACT_APP_JWT_TOKEN="sometoken0123456789"
But in the Netlify dashboard, the variables need to be given without the quote marks. They'll be read as strings no matter what, so no need to format them as such. No wonder my app kept trying to call my string-interpolated API endpoint but it wasn't working...
What if you need to pass in some parameters to your URL? Let's say you want to get a single restaurant:
const axios = require("axios");
require("dotenv").config();
exports.handler = async (event) => {
const { slug } = event.queryStringParameters;
let response
try {
response = await axios.get(`${process.env.REACT_APP_PROD_API_URL}/streets/${slug}`, {
headers: {
"Authorization": `JWT ${process.env.REACT_APP_PROD_JWT_TOKEN}`,
"Accept": "application/json",
"Content-Type": "application/json"
}
})
...
Note that although we are writing our code in JavaScript, the event.queryStringParameters
actually comes from the language-agnostic AWS Lambda ecosystem.
Building & deploying functions
This may be a good point to actually build your function and see if it works in production. If your app isn't fully ready, don't worry too much β Netlify first assigns you a completely nonsensical site name, so nobody will be able to find it. But I actually wish I'd deployed my functions earlier on, in order to be sure that they were working.
Firstly, you need to build your project by running netlify build
. This command builds both your frontend app and your severless functions. While this command runs, you can watch the logs to see whether your functions built successfully.
Does everything look good? Then you can then run netlify deploy
. If it's your first time doing this, you will need to do a bit of setup first β there's info on how to do that here.
You can go to your Netlify dashboard to check the deploy status of your project, then click the link. Hopefully your site will be live. If not, it could be down to the functions. Navigate to the functions part of the dashboard, click a function, and with the function log console open, open another tab and call your function β for example, https://my-site-name.netlify.app/get-restaurant-list
. If it doesn't give the expected response, you can then look at the logs for that function in the console, and hopefully, figure out any errors.
Routing
One of the things that I found a bit confusing while debugging my project β due to lack of explicit Netlify documentation on it, honestly β was how to routes with the routing of my React app.
Keep in mind that there are three different types of routes that we're dealing with here:
- API endpoint
- Lambda function
- React page route
Since I was using React Router for the different pages of my React website, it was pretty straightforward to link a route to a component βall I had to do was declare it in App.js
. For example, a RestaurantList
component, showing a list of restaurants, would map to https://my-restaurant-app.com/restaurants
β with /restaurants
being the route I chose. If I was just deploying a frontend app, with no backend or proxy layer, that would be straightforward.
However, due to a combination of my app not working, unclear docs, and little demonstration of varying use cases out there, at one point I was worried that if I wanted to use lambda functions, I'd have to now access this page at https://my-restaurant-app.com/get-restaurant-list
. That doesn't look very professional at all.
But, that is not the case! To avoid confusion for other people who come across this, I'll lay it out. Within my React components, I called the endpoint that was tied to the component like this:
...
// RestaurantInstance component
useEffect(() => {
if (!restaurantSlug) {
return;
}
(async () => {
const response = await fetch(
`/.netlify/functions/get-restaurant-instance?slug=${restaurantSlug}`,
{method: "GET"}
).then((response) => response.json());
setRestaurantInstance(response);
})();
}
...
Because we have proxied the original API endpoint, instead we need to call this /.netlify/functions/
endpoint, which in turn calls the API endpoint specified in the lambda function. This path will be appended to whichever server and port we've chosen to use to run functions locally. If we go by the settings given in setupProxy.js
, an example could be:
http://localhost:9000/.netlify/functions/get-restaurant-list?slug=$mcdonalds
This will have no effect on the route by which we access the app in the browser. Let's say this is the route definition for RestaurantInstance
as defined in App.js
:
<Route path="/restaurants/:restaurant_slug" element={<RestaurantInstance/>}/>
Nothing changes. Everything else just pertains to the data that is served within that component. We can access our restaurant page, complete with restaurant data, at https://my-restaurant-app.com/restaurants/mcdonalds
.
And if we go back to the netlify.toml
file for a moment, that makes sense, too. The wildcard after from =
stands for anything after the domain (or after the slash). Netlify will then do the work to convert it to a lambda function name and use that to make the request (placeholder represented here by :splat
)
from = "/*"
to = "/.netlify/functions/:splat"
Don't forget to make sure your API service is running with an up-to-date token (added to both your .env
file and Netlify dashboard), and that both your frontend server and lambda server are running!