Building Your First Web App
- Contributor
- May 19, 2025
- 6 min read
You have spent four posts building a mental model. How the internet routes requests. How frontends and backends divide responsibility. How APIs define the contract between them. How databases persist state.
Now you build something.
Not a toy. Not a fake tutorial demo that vanishes the moment you close the tab. A real application with a frontend that renders in a browser, a backend that processes logic, and a database that remembers things after you close the window. A bookmark manager — something you will actually use.
The Architecture
Before writing a single line of code, look at the whole system. Every web app, from a bookmark manager to a billion-dollar platform, follows this structure:
┌─────────────────────────────────────────────────┐
│ BROWSER │
│ │
│ HTML Form → JavaScript (fetch) → Display │
│ │ │
└──────────────────────┼───────────────────────────┘
│ HTTP Request (POST/GET)
▼
┌─────────────────────────────────────────────────┐
│ BACKEND SERVER │
│ │
│ Route Handler → Validate → Query Database │
│ │ │
└──────────────────────────────────────┼───────────┘
│ SQL Query
▼
┌─────────────────────────────────────────────────┐
│ DATABASE │
│ │
│ bookmarks table: id, url, title, created_at │
│ │
└─────────────────────────────────────────────────┘
Three layers. Three separate concerns. The browser never talks to the database directly. The database never renders HTML. Each layer does one job and communicates through defined interfaces. This is not academic theory — it is how every production web application on the planet is structured.
Layer 1: The Frontend
The frontend is an HTML page with a form and a script. The form collects input. The script sends it to the backend and updates the page with results.
<!DOCTYPE html>
<html>
<head><title>Bookmarks</title></head>
<body>
<h1>My Bookmarks</h1>
<form id="bookmark-form">
<input type="text" id="url" placeholder="URL" required />
<input type="text" id="title" placeholder="Title" required />
<button type="submit">Save</button>
</form>
<ul id="bookmark-list"></ul>
<script src="app.js"></script>
</body>
</html>
Nothing surprising here. A form with two fields, a submit button, an empty list that will hold bookmarks. The real work happens in app.js.
Layer 2: The JavaScript Bridge
This is the code that connects the browser to the backend. It listens for form submissions, sends data to the API, and updates the DOM with the response.
const form = document.getElementById('bookmark-form');
const list = document.getElementById('bookmark-list');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const url = document.getElementById('url').value;
const title = document.getElementById('title').value;
const response = await fetch('/api/bookmarks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, title })
});
const bookmark = await response.json();
addBookmarkToPage(bookmark);
});
function addBookmarkToPage(bookmark) {
const li = document.createElement('li');
li.innerHTML = `<a href="${bookmark.url}">${bookmark.title}</a>`;
list.appendChild(li);
}
Walk through this carefully. e.preventDefault() stops the form from reloading the page — default browser behavior that would destroy your single-page experience. fetch sends an HTTP POST request to /api/bookmarks with the form data as JSON. The backend processes it and returns the saved bookmark. addBookmarkToPage takes that response and creates a new list item in the DOM.
This is the exact cycle you learned about in the API post: request, process, respond, render. Now you are writing it yourself.
Layer 3: The Backend
The backend receives HTTP requests, validates data, talks to the database, and sends responses. Here it is in Express (Node.js):
const express = require('express');
const db = require('./database');
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.post('/api/bookmarks', async (req, res) => {
const { url, title } = req.body;
const result = await db.query(
'INSERT INTO bookmarks (url, title) VALUES ($1, $2) RETURNING *',
[url, title]
);
res.json(result.rows[0]);
});
app.get('/api/bookmarks', async (req, res) => {
const result = await db.query(
'SELECT * FROM bookmarks ORDER BY created_at DESC'
);
res.json(result.rows);
});
app.listen(3000);
Two routes. POST /api/bookmarks inserts a new bookmark and returns it. GET /api/bookmarks retrieves all bookmarks. express.static('public') serves your HTML and JavaScript files. The $1, $2 parameterized query prevents SQL injection — never concatenate user input into SQL strings directly.
If Python is more your speed, the equivalent in FastAPI looks like this:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
import databases
app = FastAPI()
db = databases.Database("sqlite:///bookmarks.db")
@app.post("/api/bookmarks")
async def create_bookmark(data: dict):
query = "INSERT INTO bookmarks (url, title) VALUES (:url, :title)"
await db.execute(query, values=data)
return data
@app.get("/api/bookmarks")
async def get_bookmarks():
return await db.fetch_all("SELECT * FROM bookmarks")
Different syntax. Same architecture. Same request-response cycle. The language is a detail. The pattern is what matters.
Layer 4: The Database
The database is a single table:
CREATE TABLE bookmarks (
id SERIAL PRIMARY KEY,
url TEXT NOT NULL,
title TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Four columns. An auto-incrementing ID, the URL, the title, and a timestamp that fills itself in automatically. This is the state layer — the reason your bookmarks exist after you close the browser, restart the server, or come back next week.
PostgreSQL is the standard choice for production apps. SQLite works fine for local development and small projects. The query syntax is nearly identical for basic operations.
The Full Request Cycle
Trace a single bookmark save through the entire system:
You type a URL and title into the HTML form and click Save.
JavaScript intercepts the submit event and calls fetch('/api/bookmarks', ...) with the data as JSON.
The browser sends an HTTP POST request across the network to your backend server.
Express receives the request, parses the JSON body, and extracts url and title.
The backend executes an INSERT query against PostgreSQL.
PostgreSQL writes the row to disk and returns the new record.
Express sends the record back as a JSON response.
JavaScript receives the response, parses it, and creates a new <li> element in the DOM.
The bookmark appears on the page.
Nine steps. Three layers. One HTTP round trip. This is the same cycle that runs when you post a tweet, save a document to Google Drive, or add an item to a shopping cart. The scale differs. The pattern does not.
Deploying It Live
Building locally is step one. Putting it on the internet where anyone can access it is step two. Here is the straightforward path:
Frontend: Vercel handles static files and frontend frameworks with zero configuration. Push your HTML and JavaScript to a Git repository, connect it to Vercel, and it deploys automatically on every push. Free tier covers personal projects easily.
Backend: Railway runs your Node.js or Python server with minimal setup. Connect your backend repository, set your environment variables, and it provisions a URL for your API. The free tier gives you enough hours to run a side project.
Database: Railway includes PostgreSQL, or you can use a managed database from DigitalOcean or Supabase. Managed means someone else handles backups, updates, and uptime. For a first project, this is the correct choice — do not manage your own database server.
Connecting them: Your frontend JavaScript changes from fetch('/api/bookmarks') to fetch('https://your-api.railway.app/api/bookmarks'). You add CORS headers to your backend so the browser allows cross-origin requests. That is it. Same architecture, different URLs.
The total cost for a personal project: zero dollars. Every service listed has a free tier that handles low traffic easily.
What You Actually Built
Step back and look at what you have.
A user interface that captures input. Client-side code that communicates over HTTP. A server that processes requests and enforces logic. A database that persists state across sessions. A deployment pipeline that publishes changes automatically.
This is not a tutorial exercise. This is the architecture of every web application. The bookmark manager you just built has the same structural DNA as GitHub, Notion, or any SaaS product pulling in revenue. Those products have more features, more users, more infrastructure — but the bones are identical.
You understand DNS resolution, HTTP request-response, frontend rendering, backend routing, database queries, and deployment. You did not just read about these concepts. You connected them into a working system.
You are a developer now. Not because someone gave you permission. Because you built something real.
Learning Path Complete: What Comes Next
This was the final post in the How Web Apps Work learning path. You now have the foundation that every other web technology builds on. Where you go next depends on what interests you:
React and Next.js — Component-based frontends and server-side rendering. The dominant paradigm for modern web UIs.
Python Backend Development — Flask, FastAPI, Django. Strong ecosystem for data-heavy applications and APIs.
DevOps Fundamentals — Containers, CI/CD pipelines, infrastructure as code. How professional teams ship and operate software.
Databases Deep Dive — Schema design, indexing, migrations, query optimization. The difference between an app that works and an app that works at scale.
Pick the path that matches the thing you want to build next. The foundation does not change. The layers just get more sophisticated.


