A FULL HANDS-ON GUIDE
Combine your React Application with the FastAPI web-server
In this guide, you will learn how to package a simple TypeScript React Application into a Python package and serve it from your FastAPI Python web server. Check out the client and the server repos, if you want to see the full code. Let’s get started!
During the development process, you probably use two different IDEs:
- TypeScript or JavaScript React App window, running on a dedicated listening port (e.g., 5173) to serve the client/frontend pages.
- Python FastAPI, running on a different port (e.g., 8080) to serve a REST API.
In other words, you have two different servers running locally. Whenever you want to call your FastAPI server, the browser interacts with two different servers.
While it works fine locally (in localhost), you’ll encounter a “Cross-Origin Request Blocked” error in your browser when you deploy that code. Before taking your code to production, the best practice is to serve both client pages and REST API from the same backend web server. That way the browser will interact with a single backend. It’s better for security, performance, and simplicity.
1. Create a Simple React Application
First, in your workspace directory, let’s create a new TypeScript React application using vite:
~/workspace ➜ npm create vite@latest
✔ Project name: … vite-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Then, enter into the new project directory, install the dependencies, and run the application (http://localhost:5173):
~/workspace ➜ cd vite-project
~/workspace/vite-project ➜ npm install
~/workspace/vite-project ➜ npm run dev
You should see something like:
Now, let’s make a small addition to the template — we’ll add an async HTTP call to the future FastAPI backend to get its status:
function App() {
...
const [health, setHealth] = useState('');
useEffect(() => {
const getStatus = async () => {
const response = await fetch('/v1/health-check/liveness', {
method: 'GET',
});
let status: { [status: string]: string } = {};
try {
status = await response.json();
} catch (err) {
console.log(`failed to get backend status. ${err}`);
}
setHealth(status['status'] || 'unknown');
};
getStatus();
}, []);
return (
...
<div>Backend Status: {health}</div>
...
)
}
And now we should get something like this:
At this point, the Backend Status is unknown because we haven’t implemented it yet. No worries, we will handle that shortly. Lastly, let’s build the client for packaging it later on:
~/workspace/vite-project ➜ npm run build
The build output should create a dist folder with the final optimized code that looks like this:
└── dist/
├── assets/
├── static/
└── index.html
2. Building a Python Package
At this point, we are switching to Python. I prefer to work in a virtual environment for isolation. In a dedicated virtual environment, we will install twine and build , for creating our Python package:
~/workspace/vite-project ➜ python3 -m venv venv
~/workspace/vite-project ➜ . venv/bin/activate
~/workspace/vite-project (venv) ➜ python -m pip install --upgrade pip
~/workspace/vite-project (venv) ➜ pip install twine==5.0.0 build==1.2.1
Let’s create a new setup.py file in the root folder (vite-project), with the following content:
from setuptools import setup
from pathlib import Path
cwd = Path(__file__).parent
long_description = (cwd / "README.md").read_text()
setup(
name="vite-project",
version="0.0.1",
package_dir={"vite_project": "dist"},
package_data={"vite_project": ["**/*.*"]},
long_description=long_description,
long_description_content_type="text/markdown",
)
and run the following to create the package:
~/workspace/vite-project (venv) ➜ python setup.py sdist -d tmp
~/workspace/vite-project (venv) ➜ python -m build --wheel --outdir tmp
~/workspace/vite-project (venv) ➜ twine upload -u ${USERNAME} -p ${PASSWORD} --repository-url ${REPO_URL} tmp/*
The last line above is optional if you intend to upload your package to a remote repository such as PyPI, JFrog Artifactory, etc.
3. Create a FastAPI Python web-server
The final step is to build the Python server and use the client package. For that, we will:
- Create a new backenddirectory.
- Create a new virtual environment.
- Install the relevant packages and our client package:
~/workspace/backend ➜ python3 -m venv venv
~/workspace/backend ➜ . venv/bin/activate
~/workspace/backend (venv) ➜ python -m pip install --upgrade pip
~/workspace/backend (venv) ➜ pip install fastapi==0.110.0 uvicorn==0.29.0
~/workspace/backend (venv) ➜ pip install ~/workspace/vite-project/tmp/vite-project-0.0.1.tar.gz
Note that we installed our client package from a local path that we created earlier. If you uploaded your package to a remote repository, you can install it with:
~/workspace/backend (venv) ➜ pip install --extra-index-url https://${USERNAME}:${PASSWORD}@${REPO_URL} vite-project==0.0.1
Next, let’s create a simple Python server (2 files):
__main__.py
from distutils.sysconfig import get_python_lib
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from backend.health_router import router
from uvicorn import run
def create_app():
app = FastAPI(
title="Backend Server",
)
app.include_router(router)
client_path = f"{get_python_lib()}/vite_project"
app.mount("/assets", StaticFiles(directory=f"{client_path}/assets"), name="assets")
app.mount("/static", StaticFiles(directory=f"{client_path}/static"), name="static")
@app.get("/{catchall:path}")
async def serve_react_app(catchall: str):
return FileResponse(f"{client_path}/index.html")
return app
def main():
app = create_app()
run(app, host="0.0.0.0", port=8080)
if __name__ == "__main__":
main()
health_router.py
from typing import Literal
from typing_extensions import TypedDict
from fastapi import APIRouter, status
STATUS = Literal["success", "error", "partial", "unknown"]
class ReturnHealthcheckStruct(TypedDict):
status: STATUS
router = APIRouter(
prefix="/v1/health-check",
tags=["Health Check"],
)
@router.get(
"/liveness",
summary="Perform a Liveness Health Check",
response_description="Return HTTP Status Code 200 (OK)",
status_code=status.HTTP_200_OK,
response_model=ReturnHealthcheckStruct,
)
async def liveness() -> ReturnHealthcheckStruct:
return {"status": "success"}
In the implementation above, we added support for serving any static file from our client application by mounting the static and assets folders, as well as any other client file to be served by our Python server.
We also created a simple GET endpoint, v1/health-check/liveness that returns a simple {“status”: “success”} JSON response. That way we can ensure that our server handles both client static files and our server-side RESTful API.
Now, if we go to localhost:8080 we can see our client up and running. Pay attention to the Backend Status below, it’s now success (rather than unknown).
Summary
In this tutorial, we created a simple React Application that does a single call to the backend. We wrapped this client application as a Python package and served it from our FastAPI Python web server.
Using this approach allows you to leverage the best tools in both worlds: TypeScript and React for the frontend, and Python with FastAPI for the backend. Yet, we want to keep high cohesion and low coupling between those two components. That way, you will get all the benefits:
- Velocity, by separating front-end and backend to different repositories, each part can be developed by a different team.
- Stability and Quality, by locking a versioned client package and bumping it only when the server is ready to support a new client version.
- Safety — The browser interacts with only one backend server. We don’t need to enable CORS or any other security-compromising workarounds.
- Simplicity — By working via a single server
Packaging Your TypeScript Client into a Python Backend was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.
Originally appeared here:
Packaging Your TypeScript Client into a Python Backend
Go Here to Read this Fast! Packaging Your TypeScript Client into a Python Backend