Dr Freddy Wordingham

by Dr Freddy Wordingham

Lesson

Cloud Ops

8. Calling the backend from the frontend

Last lesson we set up our FastAPI application, which will be acting as our backend. The lesson before that set up our React frontend, where the end-user will presented with our work.

This lesson will show us how we can connect the two, by sending a simple request from the frontend interface to the backend server, and displaying the result.

šŸš Adding an Endpoint


We're going to need these dependancies:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from mangum import Mangum
from pydantic import BaseModel
import os

Okay, so so far all our backend server is doing is serving our React application at route "/". But we can add more routes!

Let's add another route to our application at "/ping", which we just use to test that everything is behaving as expected:

# Ping test method
@app.get("/ping")
def ping():
    return "pong!"

All this route does is return the string "pong!" when it is hit. But this will allow us to test that defining new routes is working as it should.

If we run the server again:

python -m uvicorn main:app --port 8000 --reload

Then we should be able to visit http://localhost:8000/ping in our browser, and see the string "pong!" appear:

āž• Adding a Sum Endpoint


Okay, so far all our backend server is doing is serving our React application at the path "/". But we can add more routes, that do more and interesting things!

Let's test things out by adding a basic endpoint, at route "/sum" which simply takes two numbers (a and b), and returns the sum of these numbers:

class SumInput(BaseModel):
    a: int
    b: int


class SumOutput(BaseModel):
    sum: int


# Sum two numbers together
@app.post("/sum")
def sum(input: SumInput):
    return SumOutput(sum=input.a + input.b)

Note we've added pydantic to our imports.

Pydantic will ensure that the data we receive and send as JSON matches our expected input and output formats. It will trigger error messages when the data is incorrect.

In the code above, we've:

  1. Imported BaseModel from the pydantic library
  2. Created a new endpoint at the route "/sum"
  3. Defined an input format called SumInput
  4. Defined an return format called SumOutput (this will be converted to JSON)

šŸŒļø Hitting our Endpoint


If we were to run the backend server and visit http://localhost:8000/sum then we wouldn't get back anything:

That's because our "/sum" endpoint is a POST request, not a GET request. And when we visit a URL in our browser we're performing a GET request.

The "/sum" endpoint must be at the end of a POST request as we need to send information in the request, in this case the values of a and b.

ā„¹ļø If we really wanted to, we could have made our method a GET request, and sent the value of a and b in "path" parameters, which would make our full URL look like: https://localhost:8000/sum/a/b. However, POST is preferable to GET for a few reasons:

  • Semantics: POST is intended for operations that cause side-effects or changes.
  • Data Encapsulation: POST hides parameters in the request body, making it more secure and clean.
  • Scalability: Easier to expand the data structure in POST than in a URL's query parameters.

So, instead we're going to need to programmatically construct and send our POST request.

āž° cURL

Before we go modifying our React app, let's check if things are working.

Run the backend server:

python -m uvicorn main:app --port 8000 --reload

Then, open another terminal and then use cURL, a command line program that can send HTTP requests:

curl -X 'POST' \
  'http://localhost:8000/sum' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{"a": 5, "b": 3}'

This is saying:

  1. We're forming a POST request
  2. to http://localhost:8000/sum
  3. with a header telling it we expect to recieve some JSON in response,
  4. and that we're also sending it JSON content,
  5. and that the input JSON is: {"a" = 5, "b": 3}

We should see a response in the terminal of { "sum": 8 }:

āŒØļø TypeScript

Right, let's trigger this from our React app.

Make a new file at frontend/src/Sum.tsx to house our component:

touch frontend/src/Sum.tsx

and within it, add the placeholder:

function Sum() {
	return <div>Sum todo...</div>;
}

export default Sum;

Then, let's update our frontend/src/App.tsx file so that it uses the new <Sum> component instead of the placeholder content we had there before:

import "./App.css";
import Sum from "./Sum";

function App() {
	return (
		<div className="App">
			<header className="App-header">
				<Sum />
			</header>
		</div>
	);
}

export default App;

If we re-build the app and run the server again:

cd frontend
npm run build
cd ..
python -m uvicorn main:app --port 8000 --reload

It will look like:

Now we have the framework set up, let's flesh out the <Sum> component:

import { useState, useRef } from "react";

function Sum() {
	const [number_a, set_number_a] = useState(0);
	const [number_b, set_number_b] = useState(0);

	const output = useRef < HTMLSpanElement > null;

	const submit_sum_request = () => {
		console.log(`Number A is: ${number_a}, Number B is: ${number_b}`);

		fetch("/sum", {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify({ a: number_a, b: number_b }),
		})
			.then((response) => response.json())
			.then((data) => {
				if (output.current != null) {
					output.current.innerHTML = data.sum;
				}
			})
			.catch((error) =>
				console.log(`A terrible, evil error has occurred: ${error}`)
			);
	};

	return (
		<div>
			<input
				type="number"
				value={number_a}
				onChange={(event) => set_number_a(Number(event.target.value))}
			></input>
			<br />
			<input
				type="number"
				value={number_b}
				onChange={(event) => set_number_b(Number(event.target.value))}
			></input>
			<br />
			<button onClick={submit_sum_request}>Add!</button>
			<br />
			<p>
				The result is: <span ref={output}>0</span>
			</p>
		</div>
	);
}

export default Sum;

Here we are:

  1. Importing the useState and useRef React hooks
  2. Defining our <Sum> tag
  3. Creating two values (number_a and number_b) to store our input
  4. Creating an output which will later our
  5. Create a function submit_sum_request which takes our two values and calls our endpoint using the built-in fetch function. then. It will transform the response into JSON then. It will read the JSON data, and put the value of data.sum into the output.current.innerHTML to be displayed. catch. The error, and report it to the console if one arises.
  6. Almost finally, return the HTML to be displayed, this includes: <input>: To accept the value of number_a <input>: To accept the value of number_b <button>: To allow the user to trigger the submit_sum_request function and submit the request <p> with <span>: To display the result, when it arrives
  7. And lastly, export default Sum so that our component tag is visible outside this file.

šŸƒ Run


Re-build and reload:

cd frontend
npm run build
cd ..
python -m uvicorn main:app --port 8000 --reload

And then we can visit http://localhost:8000 and try out our complex infrastructure which is simply adding two numbers together!

šŸ“‘ APPENDIX


šŸ† How to Run

šŸ§± Build Frontend

Navigate to the frontend/ directory:

cd frontend

Install any missing frontend dependancies:

npm install

Build the files for distributing the frontend to clients:

npm run build

šŸ–² Run the Backend

Go back to the project root directory:

cd ..

Activate the virtual environment, if you haven't already:

source .venv/bin/activate

Install any missing packages:

pip install -r requirements.txt

If you haven't already, train a CNN:

python scripts/train.py

Continue training an existing model:

python scripts/continue_training.py

Serve the web app:

python -m uvicorn main:app --port 8000 --reload

šŸ—‚ļø Updated Files

Project structure
.
ā”œā”€ā”€ .venv/
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ resources
ā”‚   ā””ā”€ā”€ dog.jpg
ā”œā”€ā”€ frontend
ā”‚   ā”œā”€ā”€ build/
ā”‚   ā”œā”€ā”€ node_modules/
ā”‚   ā”œā”€ā”€ public/
ā”‚   ā”œā”€ā”€ src
ā”‚   ā”‚   ā”œā”€ā”€ App.css
ā”‚   ā”‚   ā”œā”€ā”€ App.test.tsx
ā”‚   ā”‚   ā”œā”€ā”€ App.tsx
ā”‚   ā”‚   ā”œā”€ā”€ index.css
ā”‚   ā”‚   ā”œā”€ā”€ index.tsx
ā”‚   ā”‚   ā”œā”€ā”€ logo.svg
ā”‚   ā”‚   ā”œā”€ā”€ react-app-env.d.ts
ā”‚   ā”‚   ā”œā”€ā”€ reportWebVitals.ts
ā”‚   ā”‚   ā”œā”€ā”€ setupTests.ts
ā”‚   ā”‚   ā””ā”€ā”€ Sum.tsx
ā”‚   ā”œā”€ā”€ .gitignore
ā”‚   ā”œā”€ā”€ package-lock.json
ā”‚   ā”œā”€ā”€ package.json
ā”‚   ā”œā”€ā”€ README.md
ā”‚   ā””ā”€ā”€ tsconfig.json
ā”œā”€ā”€ output
ā”‚   ā”œā”€ā”€ activations_conv2d/
ā”‚   ā”œā”€ā”€ activations_conv2d_1/
ā”‚   ā”œā”€ā”€ activations_conv2d_2/
ā”‚   ā”œā”€ā”€ activations_dense/
ā”‚   ā”œā”€ā”€ activations_dense_1/
ā”‚   ā”œā”€ā”€ model.h5
ā”‚   ā”œā”€ā”€ sample_images.png
ā”‚   ā””ā”€ā”€ training_history.png
ā”œā”€ā”€ scripts
ā”‚   ā”œā”€ā”€ classify.py
ā”‚   ā”œā”€ā”€ continue_training.py
ā”‚   ā””ā”€ā”€ train.py
ā”œā”€ā”€ main.py
ā”œā”€ā”€ README.md
ā””ā”€ā”€ requirements.txt
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from mangum import Mangum
from pydantic import BaseModel
import os


# Instantiate the app
app = FastAPI()


# Ping test method
@app.get("/ping")
def ping():
    return "pong!"


class SumInput(BaseModel):
    a: int
    b: int


class SumOutput(BaseModel):
    sum: int


# Sum two numbers together
@app.post("/sum")
def sum(input: SumInput):
    return SumOutput(sum=input.a + input.b)


# Server our react application at the root
app.mount("/", StaticFiles(directory=os.path.join("frontend",
          "build"), html=True), name="build")


# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],    # Permits requests from all origins.
    # Allows cookies and credentials to be included in the request.
    allow_credentials=True,
    allow_methods=["*"],    # Allows all HTTP methods.
    allow_headers=["*"]     # Allows all headers.
)

# Define the Lambda handler
handler = Mangum(app)


# Prevent Lambda showing errors in CloudWatch by handling warmup requests correctly
def lambda_handler(event, context):
    if "source" in event and event["source"] == "aws.events":
        print("This is a warm-ip invocation")
        return {}
    else:
        return handler(event, context)
frontend/src/App.tsx
import "./App.css";
import Sum from "./Sum";

function App() {
	return (
		<div className="App">
			<header className="App-header">
				<Sum />
			</header>
		</div>
	);
}

export default App;
frontend/src/Sum.tsx
import { useState, useRef } from "react";

function Sum() {
	const [number_a, set_number_a] = useState(0);
	const [number_b, set_number_b] = useState(0);

	const output = useRef < HTMLSpanElement > null;

	const submitSumRequest = () => {
		console.log(`Number A is: ${number_a}, Number B is: ${number_b}`);

		fetch("/sum", {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
			},
			body: JSON.stringify({ a: number_a, b: number_b }),
		})
			.then((response) => response.json())
			.then((data) => {
				if (output.current != null) {
					output.current.innerHTML = data.sum;
				}
			})
			.catch((error) =>
				console.log(`A terrible, evil error has occurred: ${error}`)
			);
	};

	return (
		<div>
			<input
				type="number"
				value={number_a}
				onChange={(event) => set_number_a(Number(event.target.value))}
			></input>
			<br />
			<input
				type="number"
				value={number_b}
				onChange={(event) => set_number_b(Number(event.target.value))}
			></input>
			<br />
			<button onClick={submitSumRequest}>Add!</button>
			<br />
			<p>
				The result is: <span ref={output}>0</span>
			</p>
		</div>
	);
}

export default Sum;