Quick Start
Build a working notes API with a frontend in about 5 minutes. We'll start from zero and progressively add database support.
Prerequisites
- Python 3.10+
- PostgreSQL (only needed for the database section)
Step 1 — Install
pip install vesta-web
Step 2 — Scaffold a project
Run vesta init in an empty directory and answer the prompts.
For this guide choose BaseServer (HTTP) and skip the ORM for now.
mkdir mynotes && cd mynotes
vesta init
The scaffolder creates:
mynotes/
├── server.py # your app
├── server.ini # configuration
├── static/
│ └── index.html # served as /
└── requirements.txt
Step 3 — Configuration
Open server.ini. The minimal config for a local server:
[server]
IP = 127.0.0.1
PORT = 9876
DEBUG = true
DEBUG = false and IP = 0.0.0.0 in production.
Step 4 — A minimal server
Replace server.py with:
from vesta import BaseServer, HTTPError
from os.path import abspath, dirname
import json
PATH = dirname(abspath(__file__))
# In-memory store — we'll swap this for the DB in step 7
_notes = []
class App(BaseServer):
@BaseServer.expose
def index(self):
# Serve the HTML shell
return self.file(PATH + "/static/index.html")
@BaseServer.expose
def api_notes(self):
self.response.type = "json"
return json.dumps(_notes)
@BaseServer.expose
def api_add(self, text=""):
if not text:
raise HTTPError(self.response, 400, "text is required")
note = {"id": len(_notes), "text": text}
_notes.append(note)
self.response.type = "json"
return json.dumps(note)
@BaseServer.expose
def api_delete(self, id=""):
global _notes
_notes = [n for n in _notes if str(n["id"]) != str(id)]
self.response.type = "json"
return json.dumps({"ok": True})
App(path=PATH, configFile="/server.ini")
Step 5 — A minimal frontend
Replace static/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Notes</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 0 16px; }
input { border: 1px solid #ccc; padding: 8px; border-radius: 4px; width: 70%; }
button { padding: 8px 14px; border: none; background: #f43f5e; color: white; border-radius: 4px; cursor: pointer; }
li { margin: 8px 0; }
span.del { cursor: pointer; color: #f43f5e; margin-left: 8px; font-size: .8rem; }
</style>
</head>
<body>
<h2>Notes</h2>
<div>
<input id="txt" placeholder="New note…">
<button onclick="add()">Add</button>
</div>
<ul id="list"></ul>
<script>
async function load() {
const data = await fetch('/api_notes').then(r => r.json());
document.getElementById('list').innerHTML =
data.map(n => `<li>${n.text}<span class="del" onclick="del(${n.id})">delete</span></li>`).join('');
}
async function add() {
const text = document.getElementById('txt').value.trim();
if (!text) return;
await fetch(`/api_add?text=${encodeURIComponent(text)}`, { method: 'POST' });
document.getElementById('txt').value = '';
load();
}
async function del(id) {
await fetch(`/api_delete?id=${id}`, { method: 'POST' });
load();
}
load();
</script>
</body>
</html>
Step 6 — Run it
python server.py
Open http://localhost:9876. You have a working notes app.
Step 7 — Add a database
Now let's persist notes to PostgreSQL. Make sure you have a running Postgres instance, then create the database:
vesta db create # creates the db user and database
vesta db init # runs db/schema.sql
Create db/schema.sql:
CREATE TABLE notes (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL
);
Add the DB section to server.ini:
[DB]
DB_USER = notes_user
DB_PASSWORD = secret
DB_HOST = localhost
DB_PORT = 5432
DB_NAME = notes_db
Update server.py to use the database and switch to Server:
from vesta import Server, HTTPError
from os.path import abspath, dirname
import json
PATH = dirname(abspath(__file__))
class App(Server):
@Server.expose
def index(self):
return self.file(PATH + "/static/index.html")
@Server.expose
def api_notes(self):
self.response.type = "json"
notes = self.db.getFilters("notes", []) # all rows
return json.dumps(notes if notes else [])
@Server.expose
def api_add(self, text=""):
if not text:
raise HTTPError(self.response, 400, "text is required")
note_id = self.db.insertDict("notes", {"text": text}, getId=True)
self.response.type = "json"
return json.dumps({"id": note_id, "text": text})
@Server.expose
def api_delete(self, id=""):
self.db.deleteSomething("notes", id)
self.response.type = "json"
return json.dumps({"ok": True})
App(path=PATH, configFile="/server.ini")
Step 8 — Add authentication (optional)
To restrict the API to logged-in users, call self.getUser() at the
start of any route. It returns the current user's ID or redirects to /auth
if the session is expired.
@Server.expose
def api_add(self, text=""):
uid = self.getUser() # raises HTTPRedirect if not logged in
if not text:
raise HTTPError(self.response, 400, "text is required")
note_id = self.db.insertDict("notes", {"text": text, "user_id": uid}, getId=True)
self.response.type = "json"
return json.dumps({"id": note_id, "text": text})
UniAuth handles the full login/register/verify flow for you. See the UniAuth docs for setup details.
What's next?
Server & Routing
Deep-dive into responses, cookies, file uploads and WebSockets.
Query Builder
All DB methods: filters, joins, upsert and table management.
UniAuth
Set up shared authentication with JWT and email verification.
JS Framework
Replace raw fetch calls with vesta's reactive frontend library.