The Ultimate FastAPI Tutorial Part 12 - Setting Up a React Frontend
In part 12 of the FastAPI tutorial, we'll look at setting up a React frontend
Introduction
Welcome to the Ultimate FastAPI tutorial series. This post is part 12. The series is a project-based tutorial where we will build a cooking recipe API. Each post gradually adds more complex functionality, showcasing the capabilities of FastAPI, ending with a realistic, production-ready API. The series is designed to be followed in order, but if you already know FastAPI you can jump to the relevant part.
Code
Project github repo directory for this part of the tutorial
Tutorial Series Contents
Optional Preamble: FastAPI vs. Flask
Beginner Level Difficulty
Part 1: Hello World
Part 2: URL Path Parameters & Type Hints
Part 3: Query Parameters
Part 4: Pydantic Schemas & Data Validation
Part 5: Basic Error Handling
Part 6: Jinja Templates
Part 6b: Basic FastAPI App Deployment on Linode
Intermediate Level Difficulty
Part 7: Setting up a Database with SQLAlchemy and its ORM
Part 8: Production app structure and API versioning
Part 9: Creating High Performance Asynchronous Logic via async def
and await
Part 10: Authentication via JWT
Part 11: Dependency Injection and FastAPI Depends
Part 12: Setting Up A React Frontend
Part 13: Using Docker, Uvicorn and Gunicorn to Deploy Our App to Heroku
Part 14: Using Docker and Uvicorn to Deploy Our App to IaaS (Coming soon)
Part 15: Exploring the Open Source Starlette Toolbox - GraphQL (Coming soon)
Part 16: Alternative Backend/Python Framework Comparisons (i.e. Django) (Coming soon)
Post Contents
Theory Section - How Frontends Interact with FastAPI
Practical Section 1 - Setting Up React Create App
Practical Section 2 - Calling FastAPI from the Frontend
Practical Section 3 - React Auth with FastAPI and JWTs
Practical Section 4 - FastAPI Updates
Theory Section - How Frontends Interact with FastAPI
So far in this tutorial series we’ve only interacted with our API via the Open API (swagger) UI, and by serving a fairly limited Jinja2 template. If your system is all backend microservices, then this is fine. However, if you’re looking to serve a more complex frontend then you’ll probably want to leverage a modern JavaScript framework like:
Whilst each of these frameworks has their pros and cons, their typical interaction with FastAPI is quite similar.
Here is a rough architecture diagram:
As shown in the diagram, the fundamental way the frontend frameworks interact with the backend is by making HTTP calls via AJAX. The typical interface to the backend is a REST API
A variation on this architecture would be a GraphQL approach, but that is not what we will focus on in this post.
You could implement the backend in any language (node, PHP, Java…any language that can create a web server), but since this is a FastAPI tutorial, the Python choice is made for us :)
Since React is the most popular of the modern frontend frameworks, this is the one I have chosen to use for the tutorial series. Whilst this isn’t a series on React, I will cover it in enough detail to give a meaningful example of how it would work with FastAPI - so I’m including common requirements like auth, and including multiple pages and components. My idea here is that if you combine this tutorial with other dedicated React tutorials (I recommend the getting started docs), you’ll be able to put together all the pieces you need for interaction across the full stack.
Note, we’ll look at deployment of both the front and backends later in the tutorial series.
Practical Section 1 - Setting Up React Create App
If you’re not familiar with React then I suggest checking out the very approachable docs. The key thing to note here is that the create-react-app package we’re making use of is an officially supported tool that simplifies React apps:
Create React App is an officially supported way to create single-page React applications. It offers a modern build setup with no configuration.
Under the hood, React relies on:
- webpack: a static module builder
- babel: a JavaScript compiler mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript)
- ESLint: A powerful code linter
Each of these usually requires configuring, and it can be a painful hurdle for those unfamiliar with the ecosystem. Create React App
basically
sets sensible defaults for you so you can skip all that setup. At any point you can call the eject
command (which is irreversible) and then all
the underlying config files are revealed so you can customize them. Given this simplification, it’s a great tool for when you are starting out, and
it offers flexibility as your app grows in complexity.
Setting Up the Project Structure
You’ll notice that in part 12 of our project repo we have a
new frontend
directory where all React code will be. To get started, cd
into this directory then install the dependencies:
- Note that
create-react-app
requires NodeJS 14+ - Run
npm install
in the directory where yourpackage.json
file is located (this lists our dependencies) - To start the React app run
npm start
Your app will start, and if you then navigate over to http://localhost:3000/
you should see this:
Terminal/CMD Prompt
:
localhost:3000
At the moment the recipes are being fetched from an externally running version of our FastAPI app (which is deployed
using the method we’ll explore in the next part of this tutorial series). This API is specified in our frontend/config.js
file
under the REACT_APP_API_BASE_PATH
setting. However, to test everything locally we’ll want to update this value to be our
running backend application: http://localhost:8001
. Obviously we’ll first have to start up our backend app, as we have
done in the previous entries in the tutorial series.
Before we hook up the local backend, let’s inspect our frontend structure:
part-12-react-frontend/frontend/src
.
├── App.css
├── App.js
├── client.js
├── components
│ ├── Button
│ │ └── Button.jsx
│ ├── DashboardHeader
│ │ ├── index.jsx
│ ├── Footer
│ │ ├── index.jsx
│ ├── FormInput
│ │ └── FormInput.jsx
│ ├── Idea
│ │ └── index.jsx
│ ├── IdeaTable
│ │ └── index.jsx
│ ├── Loader.jsx
│ ├── Modal
│ │ └── PopupModal.jsx
│ ├── Recipe
│ │ └── index.jsx
│ └── RecipeTable
│ └── index.jsx
├── config.js
├── index.css
├── index.js
├── pages
│ ├── error-page
│ │ └── index.jsx
│ ├── home
│ │ └── index.jsx
│ ├── ideas
│ │ ├── index.jsx
│ ├── login
│ │ └── index.jsx
│ ├── my-recipes
│ │ ├── index.jsx
│ │ └── NotLoggedIn.jsx
│ └── sign-up
│ ├── index.jsx
This is a pretty standard structure:
pages
are our main “containers” of HTML, representing different pages on the site as the name impliescomponents
are the building blocks which make up these pages, such as forms, buttons, tables and modals.App.js
is where we set up the routing logic which assigns page React functions to a particular url of the site.
We’ll get to the more advanced parts (like client.js
a little further on in this post).
Sidebar: What is JSX?
You’ll notice that all of our page and component files have a .jsx
extension. This is called JSX, and it is a syntax
extension to JavaScript. The React docs recommend using it to describe what the
UI should look like. JSX may remind you of a template language, but it comes with the full power of JavaScript.
Sidebar: What’s With All the ClassNames - Tailwind CSS
Within the .jsx
files we’re styling the HTML with Tailwind CSS (this is what
all the className="flex items-center justify-center
type of lines are about) which
has gained popularity over the past few years due to its ease of use, especially for responsive design.
Sidebar: React Hooks
Hooks
were added in React 16.8. If you wrote React before this update and
you’re now looking at this tutorial, you might be wondering where all the React classes are. The answer is: They’re not
needed if we use hooks.
From the docs:
Hooks let you use more of React’s features without classes. Conceptually, React components have always been closer to functions. Hooks embrace functions, but without sacrificing the practical spirit of React
Practical Section 2 - Calling FastAPI from the Frontend
Now that we understand how our React frontend application works, let’s have it fetch data from our FastAPI backend.
In frontend/src/pages/home/index.jsx
We have the following component:
Let’s break this down
- We define our React
MainView
reusable component as a function (note this is the functional component style, see this guide for an overview of the differences vs. class based components) - Because we’re using functional components, we use React hooks such as useState
- We make use of the powerful
useEffect
hook (docs)), which allows us to perform side effects in function components. Note that the empty array passed in as the second argument indicates this effect will only be called once (this empty array is the default so could be omitted, but I’m including it for clarity) - We use our instantiated
FastAPIClient
(more on this soon) to call the backend API. The response data is gathered via.then
syntax promise chaining. In Javascript there are two main ways to handle asynchronous code:then/catch
(ES6) andasync/await
(ES7). Although not as modern as theasync/await
syntax, I think it’s more useful for backend devs who might not be as up-to-date with modern JS to easily follow along (see here for a comparison of the two approaches) - We set the recipe data fetched from the API using the state hook.
OK, let’s go deeper now and look at what the client FastAPIClient
getSampleRecipes
method is doing:
client.js
- We use the JavaScript Axios promise-based HTTP client, which includes session support we will make use of.
- We instantiate the
apiClient
, which is an Axios session - The
apiBasePath
URL comes from theconfig.js
file - for local development this ishttp://localhost:8001
, but you’d update it via environment variable for deployment (we’ll cover this in the next post in the series). Note that we append the/api/v1
to match our recipe API structure. All subsequent requests with this client will automatically have this base URL prepended. - We make use of Axios request interceptors which allow us to update the request headers for auth purposes (more on auth shortly).
- Using the Axios session we call our
recipes/search?keyword
endpoint and return the response data.
The recipes/search endpoint should be familiar from previous parts of the tutorial
In order to see this client working, let’s start the backend:
- Follow the setup in the README.md
- Create the DB tables with:
poetry run ./prestart.sh
- Start the FastAPI server with
poetry run ./run.sh
- Update your frontend/config.js to set REACT_APP_API_BASE_PATH to
http://localhost:8001
(note do not use https with localhost)
- In a separate terminal/command prompt, start your React app (or if it is already running, refresh the page at
localhost:3000
)
You should now see recipe data:
And if you open up the browser dev tools and look at the network tab, you’ll see the app making requests to the backend running at http://localhost:8001:
Great! Now let’s look at user auth.
Practical Section 3 - React Auth with FastAPI and JWTs
As promised, we’re going beyond just a toy example. We’ll hook up an auth mechanism between our React frontend and our JWT-based backend auth system which we covered in part 10
We’ll start with the /frontend/src/pages/sign-up/index.jsx
We have a pretty standard React registration form, which makes use of our FormInput
and Button
components located:
frontend/src/components/FormInput/index.jsx
frontend/src/components/Button/index.jsx
sign-up/index.jsx
This is pretty standard React code, but for those backend devs who might be a bit rusty (I sympathise!), here’s a quick breakdown:
- We update the form input fields with the React
useState
hook - The
useNavigate
hook is from the react-dom-router library, which is used for routing. - We’re using our FastAPI client
register
method to call the backend (we’ll look at this next) - Upon successful registration we navigate to the
/my-recipes
page - Standard React form submission code, indicating that we’ll call the
onRegister
function when the form is submitted.
OK great, next up we’ll dig down into the client:
client.js
Here we prepare all the registration form data gathered in our React component. This loginData
is then set as the body
of a POST request to our API /auth/signup
endpoint. We return the response (which will be the JWT token). Feel
free to play around with this endpoint yourself by starting up the backend, navigating to the /docs interactive UI and
trying it out:
So now we have registered a user. We still need to login.
The login React page follows almost exactly the same format and logic as the registration page (obviously calling the login
method on the client instead of the register
method) so I won’t go over that. Where
things differ is in the client:
client.js
Lots happening in this code block, let’s break it down:
- The Authorization request header is the key header that must be set correctly for a valid login. We start by deleting it to ensure a stale default value is not used by the session.
- We POST the login data to the backend
/auth/login
endpoint - We store the response (which will be a JWT) in the browser local storage
- We’re now in possession of a JWT, and we use that to fetch the user data via the
fetchUser
method - This involves making a GET request to the backend
/auth/me
endpoint - And then we store the response user data in local storage (as well as the token)
Inspecting LocalStorage:
Every request the React app makes to the backend API has an Authorization
header inserted via the localStorageTokenInterceptor
we specified earlier in the getApiClient
method. Let’s look at the interceptor function, which is the final piece in our
client-side auth story:
Key lines to note:
- We use the jwt-decode library to decode the token - note that decoding is not the same as validating which can only be done on the server where the JWT secret resides (part 10 goes over JWT theory if you need a refresher)
- We then check the expiry data of the JWT using the Moment.js library (you can use a more modern alternative if you prefer)
- Finally, we set the
Authorization
header for the request
And voila, now requests to our FastAPI endpoints which require user auth are possible. In our React app, this allows us to have
the concept of login-required pages. The pages/my-recipes
page is an example of this. We can set component state based on the
presence of a token:
my-recipes/index.jsx
And then display different HTML and/or redirect based on this state. In our example React app the “my-recipes” page is only displayed to logged in users, and attempts to create new recipes for non-logged in users will fail.
We’re also able to chain calls to get user information and then create recipes for a specific user
based on the ID (recall, this is how the POST /recipes/
endpoint is structured, expecting a submitter_id
as one of the
POST body fields):
my-recipes/index.jsx
We’ve mostly considered the frontend additions so far, but we also need to take a moment to look at a few key updates to the API. —
Practical Section 4 - FastAPI Updates
FastAPI CORS With Frontends (like React)
CORS or “Cross-Origin Resource Sharing” refers to the situations when a frontend running in a browser has JavaScript code that communicates with a backend, and the backend is in a different “origin” than the frontend.
Quoting from the docs:
So, let’s say you have a frontend running in your browser at http://localhost:8080, and its JavaScript is trying to communicate with a backend running at http://localhost (because we don’t specify a port, the browser will assume the default port 80). Then, the browser will send an HTTP OPTIONS request to the backend, and if the backend sends the appropriate headers authorizing the communication from this different origin (http://localhost:8080) then the browser will let the JavaScript in the frontend send its request to the backend. To achieve this, the backend must have a list of “allowed origins”.
In short, this gives you control over which frontends can call your API, which is often useful. In the case of our Recipe API and React frontend, we do need to allow some origins to call our API, such as localhost (for local development) and our deployed frontend application.
We do this in two places. The first is by using the FastAPI CORS Middleware.
We add this to main.py
like so:
backend/app/app/main.py
Above, when BACKEND_CORS_ORIGINS
is set in our settings, then the CORS Middleware is applied, and the allowed_origins
are set via a list comprehension on the BACKEND_CORS_ORIGINS
value. This brings us to the second update, which is
introducing this setting:
backend/app/app/core/config.py
Notice how BACKEND_CORS_ORIGINS
includes both localhost and our deployed frontend application (on Heroku). We’ll
be looking at the Heroku deployment in the next part of the tutorial. Wherever you deploy your frontend, you’ll need
to update this value to reflect it. Once the value is set correctly, you’ll be able to call your API without any
CORS errors.
Phew! That was a lot of information. But now we have a truly modern frontend to interact with our FastAPI backend. Now we need to deploy everything…that’s coming up next.
Continue Learning FastAPI
Next we’ll deploy everything!
Part 13: Using Docker and Uvicorn to Deploy Our App to Heroku
Finding this useful? Please subscribe to my email list for updates.