Kickstart your coding journey with our Python Code Assistant. An AI-powered assistant that's always ready to help. Don't miss out!
Building full-stack web applications using Python offers a powerful and versatile approach to creating robust and interactive web experiences. In this article, we will delve into the world of full-stack development by building a Notes app using FastAPI and React.js.
You can check out the full source code of the project for this article in these links:
In this tutorial, we will build a simple Notes app project to help demonstrate full-stack development in Python with FastAPI and React.js.
Here’s a demo of the Notes app that we’re going to build in this tutorial:
The Notes app itself is a Single Page App (SPA) built with React.js that connects to the RESTful API made with FastAPI. This RESTful API handles the connection to the database and manages the CRUD (Create, Read, Update, Delete) operation.
React.js - a JavaScript library used for building user interfaces, providing an efficient and declarative way to create interactive components for web applications.
FastAPI - a modern, high-performance Python web framework that makes it easy to build robust APIs with minimal code and maximum efficiency.
SQLite - a lightweight, embedded relational database management system that provides a self-contained, serverless, and file-based solution for storing and retrieving structured data.
Before proceeding with the tutorial, make sure you have the following on your device:
Also, it’s worth mentioning that this tutorial is not aimed at absolute beginners. It is assumed that you already know how to code in Python and JavaScript. Basic knowledge in networking (i.e. HTTP protocols, HTTP requests, and responses, etc.) is also required since we will build a backend system where we need to handle some networking stuff.
Alright, let’s begin with building the Python backend. Create a new folder for the backend and instantiate a new Python virtual environment in it:
$ mkdir NotesBackend && cd NotesBackend
$ python3 -m venv venv_backend
Activate this virtual environment using this command if you’re using Linux or MacOS:
$ source venv_backend/bin/activate
Or if you’re using Windows, use this command instead:
$ venv_backend\Scripts\activate
Next, we need the dependencies of our backend app. Install FastAPI and SQLAlchemy using pip
:
$ pip install fastapi sqlalchemy
With the dependencies installed, create the files and folders for the backend following this file structure:
NotesBackend
├── main.py
├── model
│ ├── __init__.py
│ ├── database.py
│ └── models.py
├── requirements.txt
└── schemas.py
Alright, now that our new Python project is instantiated, it’s time to build the backend.
We will start by building the data layer of our backend. This data layer is where we’ll define the ORM (Object Relational Mapping) for the database tables.
Basically, this data layer will abstract away the database operations for us and what we will use to interact with the database.
We will start by instantiating the database session of SQLAlchemy, as well as doing some setup for our ORM. To do that, copy this code into your database.py
file under the models
package:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DB_URL = "sqlite:///./db.sql"
engine = create_engine(SQLALCHEMY_DB_URL, echo=True)
DBSession = sessionmaker(engine, autoflush=False)
The database URL here points out to the db.sql
file, which may not exist yet but will be auto-generated by SQLAlchemy and will be saved in your project’s directory. This database URL is then used by SQLAlchemy to refer to this database. This database reference (saved in the engine
variable) is used by the session maker to create a database session instance (DBSession
) which we will use to connect to and interact with this database later.
Alright, now it’s time for data modeling.
In our Notes App, we only have one data model, which is a Note
object. So in our case, we will only define one data model, the Note
object.
This Note
object should have the following attributes:
id
: int
- The identifier of this Note
object.title
: string
- The note title.note_body
: string
- The note content (the note itself).To define the Note
model, copy this code, and paste it to your models.py
file:
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "notes"
id = Column(Integer, primary_key=True)
title = Column(String)
note_body = Column(String)
def __repr__(self):
return f'Note(id={self.id}, title={self.title}, note_body={self.note_body})'
We named our table as notes
(with the __tablename__
attribute) since the convention is to name your database table as the plural of the objects it holds.
And as specified in our Note
object, we modeled our database to have id
(specified as the primary_key
), title
, and note_body
attributes. These attributes correspond to the columns of the notes
table in the database.
Alright, and now for the final step in building our data layer.
It’s important to ensure that there’s always a table in the database that corresponds to our defined models. For this, we can do a little hack and define this snippet of code in the __init__.py
file of the model
package:
from . import models, database
models.Base.metadata.create_all(bind=database.engine)
Every time we import the model
package (or import from it), this snippet of code will run. This code checks if all the models you defined have corresponding tables that exist in the database, otherwise, SQLAlchemy will create them for you.
Now that we have our data layer in place, it’s time to build the API. We will use the data layer we created earlier and hook it to our app’s RESTful API which we will build using FastAPI.
So first, copy this code to your main.py
file:
from fastapi import FastAPI
app = FastAPI()
Next, we need to define the API routes. The API routes are where we will handle the logic of the CRUD operations of our Notes app. For reference, these are the API routes we need to build for our API:
CRUD | API Route | HTTP Method | Description |
Create | /note |
POST |
Add new notes. |
Read | /notes |
GET |
Get all the notes in the database |
Update | /note/{note_id} |
PUT |
Update a note by its note_id |
Delete | /note/{note_id} |
DELETE |
Delete a note by its note_id |
Alright, for the first API route, we need to query and fetch all the notes in the database. This CRUD operation should be handled by the /notes
route.
Add this code into your main.py
file:
from fastapi import FastAPI
###
# Add these imports
from model.database import DBSession
from model import models
###
app = FastAPI()
###
# Add this API Route
@app.get("/notes")
def read_notes():
db = DBSession()
try:
notes = db.query(models.Note).all()
finally:
db.close()
return notes
###
All the GET
requests to the /notes
route will be handled by this function.
The route handler is just a function (read_notes
) decorated by the .get()
method of the FastAPI app
instance. This .get()
method requires a parameter, which is a string representation of the route it should point to (which in this case is the /notes
route).
For the logic of this route, it uses the database session instance (DBSession
) of our data layer to query all the entries in the notes
table. We wrapped this in a try/finally
block because we want the database connection to always close regardless if the database operation succeeds or not (as specified in the finally
block).
Finally, we return the resulting notes
query as the HTTP GET response of this API route.
Next on our CRUD operations is to handle adding new notes
to the database.
For this, we will create the /note
route that accepts POST
requests. This route will then expect a JSON data payload in its HTTP request body, which contains the data for the newly added note. This data payload is what this route will use to add new notes
to the database.
Add this route handler to the main.py
file:
...
@app.post("/note")
def add_note(note: NoteInput):
db = DBSession()
try:
if len(note.title) == 0 and len(note.note_body) == 0:
raise HTTPException(
status_code=400, detail={
"status": "Error 400 - Bad Request",
"msg": "Both 'title' and 'note_body' are empty. These are optional attributes but at least one must be provided."
})
new_note = models.Note(
title=note.title, note_body=note.note_body
)
db.add(new_note)
db.commit()
db.refresh(new_note)
finally:
db.close()
return new_note
Your IDE might give you some warnings regarding the note
parameter’s type; NoteInput
. Ignore this for now as we will get back to this later, so bear with me here for a moment.
This route is expecting a data payload for every HTTP request’s body. The shape of the object is defined using the NoteInput
type.
If the note title
and note_body
are both empty, this route handler will raise an HTTP 400 Error - Bad Request. Otherwise, it will save the note to the database.
If the data payload meets our requirement, this route handler will add this new note to the database via the data layer, then return the newly added note.
Before proceeding, let’s discuss the NoteInput
type. The NoteInput
, basically is just a schema or type definition of what the JSON data payload’s shape this route is looking for. FastAPI will use this type definition to check whether the client passed the correct shape of the JSON data it needs. If the data payload’s shape is incorrect, FastAPI will automatically raise an error to the client. But for the content of that data payload, it’s up to us to check it in the route handler. That’s why we have an if
check from earlier regarding the data payload’s content.
Alright, now let’s define the NoteInput
schema. Copy this code into your schemas.py
file at the root of your project’s directory:
from pydantic import BaseModel
class NoteInput(BaseModel):
title: str = ''
note_body: str = ''
We use Pydantic here (which comes with the FastAPI package installation) to define the data types of the expected data payload for our route handler. This is what FastAPI uses to check the shape of the data payload (as well as parse it).
Now, import this to main.py
and the IDE warnings should be gone:
from schemas import NoteInput
# other imports
...
# the rest of the code
...
Alright, let’s move on to the note update handler.
The logic for updating a note will require two database operations. First is to query the updated note through its id
, and update the resulting note
query’s data.
This route will require the client to pass the note
's id
through the dynamic route. And like the add note route, the update route will also require a JSON data payload, which contains the update data, for every PUT
request it receives. Moreover, it also uses the same NoteInput
schema for the JSON data payload, so we can reuse the NoteInput
schema from earlier.
Add this code to your main.py
file:
@app.put("/note/{note_id}")
def update_note(note_id: int, updated_note: NoteInput):
if len(updated_note.title) == 0 and len(updated_note.note_body) == 0:
raise HTTPException(status_code=400, detail={
"status": "Error 400 - Bad Request",
"msg": "The note's `title` and `note_body` can't be both empty"
})
db = DBSession()
try:
note = db.query(models.Note).filter(models.Note.id == note_id).first()
note.title = updated_note.title
note.note_body = updated_note.note_body
db.commit()
db.refresh(note)
finally:
db.close()
return note
You might notice that we also checked if the data payload is correct, because like in the note add router, we also don’t want to save an empty note in the database.
And for the last API route, the delete note route.
This route is a simple one as we only need the note
's id
and check if it exists in the database. If it exists, we delete it, otherwise, we tell the user that there’s no such note exists in the database.
Add this code to your main.py
file:
from sqlalchemy.orm.exc import UnmappedInstanceError
# other imports
...
# rest of the code
@app.delete("/note/{note_id}")
def delete_note(note_id: int):
db = DBSession()
try:
note = db.query(models.Note).filter(models.Note.id == note_id).first()
db.delete(note)
db.commit()
except UnmappedInstanceError:
raise HTTPException(status_code=400, detail={
"status": "Error 400 - Bad Request",
"msg": f"Note with `id`: `{note_id}` doesn't exist."
})
finally:
db.close()
return {
"status": "200",
"msg": "Note deleted successfully"
}
Alright, all the routes are finished! But we’re not done yet here. There’s a final step we need to take before we move on to the front end.
Our full-stack app is modeled after the client-server architecture where the front-end (or the client) is separate from the back-end. If you’re a seasoned developer, I bet you probably already know that we will get a CORS error in the front-end if we don’t set up CORS at the back-end. But if you’re unaware, I recommend you check out what is CORS and what are CORS errors from MDN (Mozilla Developer Network).
So in our backend, we need to specify the domain of our front-end so that it will allow requests coming from our front-end app.
And since we’re just doing development here, you can specify the front-end’s origin as:
http://localhost:5173
This domain is the default development domain and port of Vite for React apps. And since we will use Vite to create a new React app in the front-end, you can add this to the origins list of your CORS middleware. But if you’re planning a different route for building the front-end or having a different domain name for the front-end, feel free to specify them in the origins list.
And now, going back to the main.py
file, add the CORS middleware to your API and specify the domain name of your front-end:
from fastapi.middleware.cors import CORSMiddleware
# other imports
...
app = FastAPI()
origins = [
"http://localhost:5173" # or add your own front-end's domain name
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"]
)
All the client-side domains you want to connect to this API must be specified in the origins
list. The origins
list is just a list of domain names that you allow to connect to your backend. If you decided to host your front-end in the future, you can add its domain name to this origins
list.
And with that, our backend is done! We can now proceed to build the front-end, but if you want to, you can test this API in Postman or Insomnia (which we will skip in this tutorial as this will get even longer).
Alright, let’s leave the back-end world and head on to the user-facing world of the Front-end!
Before we proceed, take note that I won’t be explaining much of the front-end code since the focus of this article is on Python code. Rather, I will explain them at a high level. For all the CSS code, I will skip explaining to keep this tutorial from getting too long.
Let’s start by creating a new React project using Vite. On a new directory (outside your back-end’s project directory), run this command to create a new React project:
$ yarn create vite
For the Vite questions:
NoteFrontend
React
JavaScript
After Vite instantiated a new React project for you, run the following commands to install all the dependencies of your fresh new React project.
$ cd NoteFrontend
$ yarn
And finally, install another dependency for this project:
$ yarn add axios@1.4.0
Axios is what we will use to send HTTP requests to our back-end. I specified the Axios version 1.4.0
here so that your project’s dependencies are kept consistent with the code in this tutorial.
I will assume you already know React, so I won’t discuss the files and folders outside the src
folder.
So in the src
folder, first, remove all the contents of the following files and leave them blank for now:
App.css
App.jsx
index.css
Now, copy the base styling CSS code from this link and paste it into the emptied index.css
file. Do the same thing for the App.css
file with the app styling from this link.
Then, follow this file structure under the src
folder by creating the specified files and folders:
NotesFrontend
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── components
│ │ ├── AddNote
│ │ │ ├── AddNote.styles.css
│ │ │ ├── AddNote.jsx
│ │ │ └── index.js
│ │ └── NotesList
│ │ ├── Note
│ │ │ ├── DeleteModal.styles.css
│ │ │ ├── DeleteModal.jsx
│ │ │ ├── Note.styles.css
│ │ │ ├── Note.jsx
│ │ │ ├── NoteEditor.jsx
│ │ │ ├── NoteViewer.jsx
│ │ │ └── index.js
│ │ ├── NotesList.styles.css
│ │ ├── NotesList.jsx
│ │ └── index.js
│ ├── index.css
│ ├── main.jsx
│ └── utility.styles.css
├── ...
Again, we’re only concerned about the src
folder so you can leave the files and folders outside src
as they are. Just follow the file structure above by creating the specified files and folders.
At the root of any React project is the main App
component. This component serves as the root of the component tree in a React project.
So for our notes app, copy this code and paste it to your empty App.jsx
file. Note that this is the final version of the app, and we will later build all the components that are imported and added to this App
component.
import { useState, useEffect, createContext } from "react";
import axios from "axios";
import AddNote from "./components/AddNote";
import NotesList from "./components/NotesList";
import "./App.css";
import "./utility.styles.css";
export const NotesListUpdateFunctionContext = createContext(null);
export default function App() {
const [notes, setNotes] = useState([]);
useEffect(() => {
const getNotes = async () => {
// The URL of the backend
// you can specify your own if you have a different one
const API_URL = "http://localhost:8000";
const { data } = await axios.get(`${API_URL}/notes`);
setNotes(data);
};
getNotes();
}, []);
return (
<NotesListUpdateFunctionContext.Provider value={setNotes}>
<div>
<h1 id="app-title">Notes App</h1>
<AddNote />
<NotesList notes={notes} />
</div>
</NotesListUpdateFunctionContext.Provider>
);
}
The logic of this App
component is that when this is mounted to the DOM, it will send an HTTP GET request to our backend to fetch all the notes in the database and render each of the notes in a <Notes />
component (which is handled by the <NotesList />
component). It also renders the <AddNote />
component, which we will discuss in the next section.
AddNote
ComponentThe AddNote
component is simply just a form component for writing and submitting new notes to the backend. This component checks if the new note added is empty or not. If the note is empty, this component won’t send the empty note to the backend and will inform the user about it. Otherwise, the new note will be sent to the API and saved to the database.
For the AddNote
component, copy this code to your components/AddNote/AddNote.jsx
file:
import { useState, useContext } from "react";
import axios from "axios";
import { NotesListUpdateFunctionContext } from "../../App";
import "./AddNote.styles.css";
export default function AddNote() {
const [title, setTitle] = useState("");
const [noteBody, setNoteBody] = useState("");
const [isFormSubmitting, setIsFormSubmitting] = useState(false);
const [hasInputError, setHasInputError] = useState(false);
const setNotes = useContext(NotesListUpdateFunctionContext);
const handleSubmit = async (event) => {
event.preventDefault();
if (title.length > 0 || noteBody.length > 0) {
setIsFormSubmitting(true);
const API_URL = "http://localhost:8000";
const { data } = await axios.post(`${API_URL}/note`, {
title,
note_body: noteBody,
});
setNotes((prev) => [...prev, data]);
} else {
setHasInputError(true);
}
setTitle("");
setNoteBody("");
setIsFormSubmitting(false);
};
return (
<form onSubmit={(event) => handleSubmit(event)} id="add-note-form">
<input
type="text"
placeholder="Enter Title"
id="title-input"
className={hasInputError ? "input-error" : ""}
value={title}
onChange={(event) => {
setHasInputError(false);
setTitle(event.target.value);
}}
/>
<textarea
placeholder="Enter Note"
id="note-body-textarea"
className={hasInputError ? "input-error" : ""}
cols={30}
rows={10}
value={noteBody}
onChange={(event) => {
setHasInputError(false);
setNoteBody(event.target.value);
}}
/>
<button id="add-note-btn" type="submit" disabled={isFormSubmitting}>
{isFormSubmitting ? "..." : "Add Note"}
</button>
</form>
);
}
For the styling of this component, copy the AddNote CSS code from this link and paste it to your components/AddNote/AddNotes.styles.css
file.
Finally, on the components/AddNote/index.js
file, import this component, and export it at the same time, making it the default export of this AddNote
package:
import AddNote from "./AddNote";
export default AddNote;
NotesList
ComponentThe next component we will build is the NotesList
component.
This component simply renders each note item to a <Note />
component. The <Note />
component is then responsible for all the logic that relates to it, such as editing and deleting the note.
Copy this code to your components/NotesList/NotesList.jsx
file:
import Note from "./Note/Note";
import "./NotesList.styles.css";
export default function NotesList({ notes }) {
return (
<div id="notes-list-container">
<h2 id="notes-list-header">NOTES</h2>
<ul id="notes-list">
{notes.map((note) => (
<li key={note.id}>
<Note note={note} />
</li>
))}
</ul>
</div>
);
}
And also for the styling, copy the CSS code from this link to the NotesList/NotesList.styles.css
file.
And like the AddNotes
component, import this on the package’s index file, and export it, making it the default component of the package:
import NotesList from "./NotesList";
export default NotesList;
Note
ComponentThe Note
component is still under the NotesList
component tree and is the component used to render each note
item.
The Note
component has two renderers, a viewer and an editor. The viewer is the default one and is simply just used for rendering the note item. The editor renderer on the other hand is used when editing the contents of the note. So basically, the Note
component is just for deciding which one of the two sub-components should be rendered.
Here is the full code for the <Note />
component:
import { useState } from "react";
import NoteViewer from "./NoteViewer";
import NoteEditor from "./NoteEditor";
export default function Note({ note }) {
const [noteView, setNoteView] = useState("viewing");
switch (noteView) {
case "viewing":
return <NoteViewer note={note} setNoteView={setNoteView} />;
case "editing":
return <NoteEditor note={note} setNoteView={setNoteView} />;
default:
return <></>;
}
}
And like before, get the styling of this component from this link and paste it to Note/Note.styles.css
file.
Also, don’t forget to export this as the default component export of this package:
import Note from "./Note";
export default Note;
Now let’s proceed to one of its sub-components, the NoteEditor
component. The NoteEditor
component is similar to the add new note component but this just has an existing note data to work with. Also, it does not allow saving an empty note to the database and informing the user if they are saving an empty note.
Here’s the code for the <NoteEditor />
component:
import axios from "axios";
import { useState, useContext } from "react";
import { NotesListUpdateFunctionContext } from "../../../App";
import { NoteView } from "./Note";
import "./Note.styles.css";
export default function NoteEditor({ note, setNoteView }) {
const [noteTitle, setNoteTitle] = useState(note.title);
const [noteBody, setNoteBody] = useState(note.note_body);
const [isInvalidSave, setIsInvalidSave] = useState(false);
const setNotes = useContext(NotesListUpdateFunctionContext);
const handleNoteSave = async (event, id) => {
event.preventDefault();
if (noteTitle.length > 0 || noteBody.length > 0) {
const API_URL = "http://localhost:8000";
await axios.put(`${API_URL}/note/${id}`, {
title: noteTitle,
note_body: noteBody,
});
const { data } = await axios.get(`${API_URL}/notes`);
setNotes(data);
setNoteView("viewing");
} else {
setIsInvalidSave(true);
noteTitleInputRef.current.focus();
}
};
return (
<form
id="note-container"
onSubmit={(event) => handleNoteSave(event, note.id)}
>
<input
type="text"
placeholder="Enter Note Title"
id="note-title-edit-input"
className={isInvalidSave ? "input-error" : ""}
value={noteTitle}
onChange={(event) => {
setIsInvalidSave(false);
setNoteTitle(event.target.value);
}}
/>
<textarea
placeholder="Enter Note"
id="note-body-edit-input"
className={isInvalidSave ? "input-error" : ""}
cols={30}
rows={5}
value={noteBody}
onChange={(event) => {
setIsInvalidSave(false);
setNoteBody(event.target.value);
}}
/>
<div className="note-buttons-container">
<button className="save-btn" type="submit">
Save
</button>
<button
className="neutral-btn"
type="button"
onClick={() => setNoteView("viewing")}
>
Cancel
</button>
</div>
</form>
);
}
It uses the same stylesheet as the Note
component, so no need to create a new stylesheet for this one.
For the other sub-component <NoteViewer />
renderer, this is just for viewing the notes, as discussed earlier.
Copy this code and paste it to the Note/NoteViewer.jsx
file:
import { useState } from "react";
import DeleteModal from "./DeleteModal";
import { NoteView } from "./Note";
import "./Note.styles.css";
export default function NoteViewer({ note, setNoteView }) {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const { id, title, note_body } = note;
const handleDeleteNote = () => {
setShowDeleteModal(true);
};
return (
<div id="note-container">
{showDeleteModal && (
<DeleteModal showDeleteModal={setShowDeleteModal} noteId={id} />
)}
<h3>{title}</h3>
<p>{note_body}</p>
<div className="note-buttons-container">
<button
className="neutral-btn"
onClick={() => setNoteView("editing")}
>
Edit
</button>
<button className="delete-btn" onClick={() => handleDeleteNote()}>
Delete
</button>
</div>
</div>
);
}
Also, it uses the same stylesheet as the Note
and NoteEditor
components, so again, no need to add a new one.
But hold on, we’re not done yet with the Note
sub-component tree. In this component, there is another sub-component, the <DeleteModal />
component. Basically, this delete modal will show up on the screen when the user clicks the delete button to ask if the user confirms this action.
For this modal component, copy this code and paste it into the /Note/DeleteModal.jsx
file:
import { useContext } from "react";
import axios from "axios";
import { NotesListUpdateFunctionContext } from "../../../App";
import "./DeleteModal.styles.css";
export default function DeleteModal({ noteId, showDeleteModal }) {
const setNotes = useContext(NotesListUpdateFunctionContext);
const handleYesClick = async () => {
const API_URL = "http://localhost:8000";
await axios.delete(`${API_URL}/note/${noteId}`);
const { data } = await axios.get(`${API_URL}/notes`);
setNotes(data);
showDeleteModal(false);
};
const handleNoClick = () => {
showDeleteModal(false);
};
return (
<div id="delete-modal-container">
<div id="delete-modal">
<p id="prompt-msg">Delete this Note?</p>
<div id="btn-container">
<button id="yes-btn" onClick={() => handleYesClick()}>
Yes
</button>
<button id="no-btn" onClick={handleNoClick}>
No
</button>
</div>
</div>
</div>
);
}
Finally, get the styling of this component in this link and paste it into the Note/DeleteModal.styles.css
file.
Now your full-stack notes app is ready! Run yarn dev
to your terminal or command prompt and play with your notes app or customize it the way you want.
That is all there is to it in building a simple Fullstack Notes app in Python with FastAPI and React.js.
This article has demonstrated the process of building a full-stack Notes app using FastAPI and React.js. By leveraging FastAPI on the back-end and React.js on the front-end, developers can create responsive and feature-rich web applications.
Hope you found this tutorial helpful. You can check out the full source code of the project for this article in these links:
Here are some similar tutorials:
Happy coding ♥
Liked what you read? You'll love what you can learn from our AI-powered Code Explainer. Check it out!
View Full Code Transform My Code
Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!