Photo by Markus Spiske on Unsplash

A FULL HANDS-ON GUIDE

Packaging Your TypeScript Client into a Python Backend

Combine your React Application with the FastAPI web-server

6 min readApr 5, 2024

--

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:

  1. TypeScript or JavaScript React App window, running on a dedicated listening port (e.g., 5173) to serve the client/frontend pages.
  2. 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.

Local development (Image by Author)

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.

Preparing for Production (Image by Author)

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:

First Vite React Template (Image by Author)

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:

With a Backend call (Image by Author)

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).

Running a Python server together with React Application (Image by Author)

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

--

--