by Dr Freddy Wordingham
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:
- Imported
BaseModel
from thepydantic
library - Created a new endpoint at the route "/sum"
- Defined an input format called
SumInput
- 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:
- We're forming a POST request
- to http://localhost:8000/sum
- with a header telling it we expect to recieve some JSON in response,
- and that we're also sending it JSON content,
- 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:
- Importing the
useState
anduseRef
React hooks - Defining our <Sum> tag
- Creating two values (
number_a
andnumber_b
) to store our input - Creating an
output
which will later our - Create a function
submit_sum_request
which takes our two values and calls our endpoint using the built-infetch
function. then. It will transform the response into JSON then. It will read the JSONdata
, and put the value ofdata.sum
into theoutput.current.innerHTML
to be displayed. catch. Theerror
, and report it to the console if one arises. - Almost finally, return the HTML to be displayed, this includes:
<input>: To accept the value of
number_a
<input>: To accept the value ofnumber_b
<button>: To allow the user to trigger thesubmit_sum_request
function and submit the request <p> with <span>: To display the result, when it arrives - 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;