Most tutorials treat the backend and the frontend like two strangers who happen to share an apartment. They each get their own chapter, their own framework, their own jargon, and you are left squinting at the gap between them wondering, “But how do they actually talk to each other?”
On this page
That gap is exactly what we are going to fill today.
We are going to build a small but fully functional Bookmark Manager. A Python backend powered by FastAPI will store and serve our data. A plain HTML, CSS, and JavaScript frontend will display it and let us interact with it. No React. No npm. No build step. Just two files and the invisible protocol that connects them.
By the end, you will not just understand full stack development as a concept. You will have felt the data flow through your fingers, from a fetch() call in the browser, across the network, into a Python function, and back again.
Let’s get cooking.
THE RESTAURANT METAPHOR
Before we write a single line of code, let’s build a mental model that will carry us through the entire tutorial.
Think of a full stack application as a restaurant.
- The kitchen is the backend. It’s where the real work happens: storing ingredients, preparing dishes, following recipes. Customers never see the kitchen. They don’t need to.
- The dining room is the frontend. It’s the beautiful, user-facing space where people sit, browse the menu, and enjoy their meal.
- The waiter is the API. The waiter takes orders from the dining room, carries them to the kitchen, waits for the chef to prepare the dish, and then carries the plate back out.
The customers (users) never shout directly into the kitchen. The chefs never wander out into the dining room. The waiter is the agreed-upon protocol. In web development, that protocol is HTTP, and the language they share is JSON.
Every single thing we build today is just an elaboration of this metaphor. Keep it in your back pocket.
SETTING THE STAGE
We need to set up our kitchen before we can cook anything. That means installing Python, creating an isolated workspace, and bringing in FastAPI.
First, make sure you have Python 3.9+ installed. Open a terminal and check:
|
|
If you see something like Python 3.11.4, you are good to go. If not, head over to python.org and grab the latest version.
Now, let’s create a project folder and a virtual environment. A virtual environment is like giving your project its own private pantry of ingredients, so nothing clashes with other projects on your machine.
|
|
Activate the virtual environment:
|
|
You should see (venv) appear at the start of your terminal prompt. That’s your cue that you are working inside the isolated environment.
Now, let’s install our two key ingredients:
|
|
FastAPI is our web framework. It handles routing, data validation, and serialization. Uvicorn is the server that actually listens for incoming HTTP requests and hands them off to FastAPI. Think of FastAPI as the head chef and Uvicorn as the maître d’ who seats the guests and calls orders to the kitchen.
YOUR FIRST ENDPOINT
Let’s prove that the kitchen works. Create a file called main.py in your project folder:
|
|
That’s it. Five lines of code and we have a working backend. Let’s break it down:
- Line 1 imports the FastAPI class.
- Line 3 creates an instance of our application. This
appobject is our entire kitchen. - Line 5 is a decorator. It tells FastAPI, “When someone sends a GET request to the root URL
/, run the function below.” - Lines 6-7 define the function. It returns a Python dictionary, and FastAPI automatically converts it to JSON for us.
Let’s fire up the server:
|
|
The --reload flag tells Uvicorn to restart automatically whenever we save changes. Very handy during development.
Open your browser and navigate to http://127.0.0.1:8000. You should see:
|
|
That, right there, is a backend talking to a frontend. Your browser made an HTTP GET request. Uvicorn received it, handed it to FastAPI, FastAPI ran our function, and the JSON response traveled back through the wire to your browser. The waiter just delivered the first dish.
Pro-Tip: FastAPI generates interactive API documentation for free. Visit
http://127.0.0.1:8000/docsand you will see a beautiful Swagger UI where you can test every endpoint without writing any frontend code. This is incredibly useful during development.
THINKING IN RESOURCES
Before we start building our Bookmark API, we need to think about what we are building. In REST (Representational State Transfer), the world is made up of resources. A resource is any “thing” your API manages. For us, that thing is a bookmark.
Each bookmark will have:
- An id (a unique number to identify it)
- A url (the web address we are saving)
- A title (a human-readable name)
- A description (an optional note about why we saved it)
Now, HTTP gives us a set of verbs (methods) that describe what we want to do with a resource. Here is the contract for our Bookmark API:
| Method | Endpoint | What It Does | Kitchen Analogy |
|---|---|---|---|
GET |
/bookmarks |
Fetch all bookmarks | “Show me the full menu” |
GET |
/bookmarks/{id} |
Fetch one bookmark | “Tell me about dish #3” |
POST |
/bookmarks |
Create a new bookmark | “I’d like to order something new” |
PUT |
/bookmarks/{id} |
Update an existing bookmark | “Actually, change my order” |
DELETE |
/bookmarks/{id} |
Delete a bookmark | “Cancel that dish, please” |
This table is our entire API contract. Every line of backend code we write will serve one of these five operations. Every line of frontend code will call one of them.
BUILDING THE BOOKMARK API
Time to build out the kitchen. We will expand main.py incrementally, starting with the data model and storage, then adding each endpoint one by one.
The Data Model
FastAPI uses Pydantic for data validation. Pydantic models are like order forms: they define exactly what shape the data should be, and FastAPI will reject anything that doesn’t match. This means we get automatic input validation for free.
Let’s update main.py:
|
|
BookmarkIn describes what the frontend must send when creating or updating a bookmark. The description field has a default of "", making it optional. Our bookmarks dictionary and next_id counter act as a simple in-memory database, our pantry of saved items.
Note: We are deliberately avoiding a real database here. The goal is to understand the communication layer, not database drivers. In-memory storage means your data disappears when you restart the server, and that is perfectly fine for learning.
Read All Bookmarks (GET /bookmarks)
Our first real endpoint fetches every bookmark in our pantry:
|
|
We convert the dictionary values to a list because JSON represents collections as arrays. When the frontend asks, “Show me everything,” we hand back a clean JSON array.
Read One Bookmark (GET /bookmarks/{id})
Sometimes the frontend wants just one specific bookmark:
|
|
Notice {bookmark_id} in the URL. FastAPI automatically extracts it from the path and converts it to an int. If someone asks for a bookmark that doesn’t exist, we raise an HTTPException with a 404 status code, the universal way of saying, “We don’t have that on the menu.”
Create a Bookmark (POST /bookmarks)
This is where the frontend places a new order:
|
|
A few things to unpack here:
- We accept a
BookmarkInobject in the request body. FastAPI reads the raw JSON from the incoming request, validates it against our Pydantic model, and hands us a clean Python object. If the JSON is malformed or missing required fields, FastAPI automatically sends back a 422 error with a detailed explanation. We didn’t write a single line of validation code. - We return status code 201 (“Created”) instead of the default 200 (“OK”). This tells the frontend, “Not only did things go well, but a new resource was created as a result.”
bookmark.model_dump()converts the Pydantic object back into a dictionary, and we spread it with**to merge it with our generatedid.
Update a Bookmark (PUT /bookmarks/{id})
The customer wants to change their order:
|
|
This combines the path parameter (bookmark_id) with a request body (bookmark). FastAPI handles both simultaneously. We overwrite the old record with the new data and return the updated version.
Delete a Bookmark (DELETE /bookmarks/{id})
And finally, canceling an order:
|
|
We return a simple confirmation message. Some APIs return 204 No Content here (literally an empty response), but returning a JSON message is more beginner-friendly because the frontend can display it.
Pro-Tip: If your server is still running with
--reload, it has already picked up every change you saved. You don’t need to restart it manually. That’s--reloadearning its keep.
TESTING WITH YOUR BROWSER AND CURL
Our kitchen is fully built. Before we construct the dining room, let’s test every dish from the kitchen window.
Remember that free documentation page? Navigate to http://127.0.0.1:8000/docs. You will see all five endpoints listed with their methods, parameters, and expected request bodies. Click on any endpoint, hit “Try it out”, fill in the fields, and hit “Execute”. FastAPI will run the request and show you the response, headers, and status code.
If you prefer the command line, curl is your friend:
|
|
Warning: On Windows Command Prompt, replace single quotes
'with double quotes"and escape the inner double quotes with\". Or, just use PowerShell or the Swagger UI at/docs, which avoids this hassle entirely.
Try creating two or three bookmarks, then fetching them. Update one. Delete one. Watch the responses. You are manually playing the role of the frontend right now, and that is exactly the right way to build confidence before we automate it.
ENTER THE FRONTEND
Our kitchen is humming along beautifully. Time to build the dining room.
Create a file called index.html in the same folder as main.py. We are keeping things radically simple: one HTML file with embedded CSS and JavaScript. No build tools, no bundlers, no node_modules folder the size of a small planet.
|
|
If you open this file directly in your browser right now (just double-click it), you will see a clean form and an empty list. It looks like an app, but it has no brain. It can’t store anything, fetch anything, or do anything useful. The dining room is beautifully decorated, but there is no kitchen behind it.
Let’s fix that.
THE FETCH API: BRIDGING THE GAP
This is the heart of the entire tutorial. The Fetch API is the waiter. It’s the browser’s built-in mechanism for sending HTTP requests to a server and receiving responses.
Add a <script> tag just before the closing </body> tag in index.html:
|
|
That is a lot of code, so let’s walk through the most important pieces.
The Anatomy of a fetch() Call
Look at the createBookmark function. The fetch() call is the exact moment the dining room talks to the kitchen:
|
|
Let’s read it like an order slip:
- URL (
${API}/bookmarks): Where to send the order. Our waiter walks to the kitchen door labeled/bookmarks. - method (
"POST"): What kind of action. POST means “create something new.” - headers (
Content-Type: application/json): Metadata about the order. We are telling the kitchen, “This order is written in JSON.” - body (
JSON.stringify(body)): The actual order details, converted from a JavaScript object into a JSON string for transport.
The await keyword pauses our function until the kitchen sends a response. When it does, we get a Response object (res). We check res.ok (which is true for any 2xx status code), and if something went wrong, we throw an error. If everything is fine, we call res.json() to parse the JSON body, just like the waiter translating the chef’s French back into English for the customer.
async and await
You might be wondering about those async and await keywords. Network requests take time. The browser can’t just freeze while it waits for the server to respond. So fetch() returns a Promise, an object that represents “a value that will arrive in the future.”
The await keyword says, “Pause this function (not the entire browser) until the Promise resolves.” And async in front of the function declaration is simply the admission ticket that allows await to be used inside.
Note: Without
async/await, we would chain.then()callbacks. Both approaches work, butasync/awaitreads like regular top-to-bottom code, which makes it much easier to follow.
CORS: THE SECURITY GUARD
If you try to open index.html in your browser right now (by double-clicking it or using a simple file server), you will hit a wall. Open the browser’s Developer Tools (press F12), check the Console tab, and you will see something like:
Access to fetch at 'http://127.0.0.1:8000/bookmarks' from origin 'null'
has been blocked by CORS policy.
Welcome to CORS, or Cross-Origin Resource Sharing. This is the browser’s built-in security guard, and it is doing its job correctly.
Here is the problem: your HTML file is running from one “origin” (either null for a file:// URL or http://localhost:5500 if you used a live server), and your FastAPI backend is running on a different origin (http://127.0.0.1:8000). The browser’s security model says, “A page from Origin A is not allowed to fetch data from Origin B unless Origin B explicitly permits it.”
This is a browser rule, not a server rule. That’s why curl worked fine in the previous section. curl is not a browser and doesn’t enforce CORS.
The fix is beautifully simple. We just need to tell our FastAPI kitchen, “Yes, I trust the dining room. Let them in.” Update main.py by adding these lines near the top, right after creating the app:
|
|
Warning: Using
allow_origins=["*"]means “allow any website on Earth to call my API.” This is perfectly fine for local development and learning. For a production application, you would replace"*"with a specific list of trusted domains, like["https://myapp.com"].
Place this import alongside the existing imports, and the add_middleware call right after app = FastAPI(). Save the file, and Uvicorn’s --reload will restart the server automatically.
Now refresh your index.html in the browser. The CORS error should be gone, and you should see your bookmarks loading (an empty list if you haven’t created any yet). Try adding a bookmark through the form. Watch it appear instantly. Delete one. Feel the data flow.
You just built a full stack application.
ERROR HANDLING ACROSS THE WIRE
A well-built restaurant doesn’t just serve great food, it handles problems gracefully. What happens if the kitchen burns a dish? The waiter should tell the customer politely, not just disappear into the back.
Our backend already handles errors through HTTPException. When we try to fetch or delete a bookmark that doesn’t exist, FastAPI returns a 404 with a JSON body like {"detail": "Bookmark not found"}. Our frontend’s showError() function catches these failures and displays them to the user.
But let’s make it a bit more robust. What if the server is completely offline? The fetch() call itself will throw a network error. Our try/catch blocks already handle this, the catch clause in each function will fire, and showError will display the message. No crashes, no blank screens, just a red message that fades after a few seconds.
Pro-Tip: Open your browser’s DevTools Network tab (F12 > Network) while using the app. You will see every single HTTP request your frontend makes: the method, the URL, the status code, the request body, and the response body. This is the single most useful debugging tool for full stack development. Get comfortable living in that tab.
THE COMPLETE PICTURE
Let’s step back and see both of our files in their final form. First, the backend:
|
|
And the frontend, all in one index.html:
|
|
Two files. About 60 lines of Python and 160 lines of HTML/CSS/JS. Together, they form a complete, working full stack application. The kitchen prepares the data, the waiter (HTTP + JSON) carries it, and the dining room displays it beautifully.
WHERE TO GO NEXT
We covered a lot of ground in this tutorial, and we intentionally kept things focused on the communication layer. But real-world applications need more. Here are the natural next steps once you are comfortable with what we built today:
- Persistent Storage: Replace our in-memory dictionary with a real database like SQLite (with SQLAlchemy or Tortoise ORM). Your data will survive server restarts.
- Authentication: Add user accounts so each person sees only their own bookmarks. FastAPI has excellent support for OAuth2 and JWT tokens.
- Deployment: Put your backend on a cloud platform like Railway, Render, or a VPS, and serve your frontend from a CDN or a static host.
- Frontend Frameworks: Once you understand the raw
fetch()calls, frameworks like React, Vue, or Svelte simply wrap them in more ergonomic patterns. You will find them much easier to learn now.
The foundation you built today, understanding how HTTP verbs, JSON, and status codes flow between the client and server, will carry you through every one of these topics.
KEY TAKEAWAYS
- Backend = Kitchen, Frontend = Dining Room, API = Waiter. The frontend never touches the data directly. It always asks the backend through HTTP requests.
- REST is a contract. Each endpoint is a combination of an HTTP method (GET, POST, PUT, DELETE) and a URL path. Design your API contract first, then implement it.
- FastAPI gives you superpowers for free. Automatic JSON serialization, request validation via Pydantic, interactive docs at
/docs, and type-safe path parameters. fetch()is the bridge. Every frontend-to-backend call in vanilla JavaScript goes throughfetch(), which returns a Promise resolved with the server’s response.- CORS is not a bug, it’s a feature. The browser protects users from malicious cross-origin requests. You explicitly opt in to allowing them with
CORSMiddleware. - Status codes are the API’s body language.
200means “OK”,201means “Created”,404means “Not Found”,422means “Your data is malformed.” Learn to read them. - The DevTools Network tab is your best friend. Use it constantly. Every request, header, and response body is visible there.