Prerequisites

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
Note Set 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?