A modular note-taking app with:
- Dashboard to create, open, and delete notes
- Per-note page with modular “blocks”:
- Text blocks
- Todo list blocks
- Table blocks (with add/delete rows and columns)
- Each block can:
- Have a custom title
- Be collapsed/expanded
- Be full-width or half-width in the grid
Stack:
- Frontend: Next.js (App Router) + TypeScript + shadcn/ui
- Backend: FastAPI + SQLAlchemy (async) + PostgreSQL (Neon)
- DB: JSONB
datafield on blocks to support flexible layouts
notes-app/
backend/
main.py # FastAPI app: models, schemas, routes
.env # Backend env vars (DATABASE_URL, DEV_USER_ID)
.venv/ # Python virtualenv (local only)
frontend/
app/ # Next.js app routes (/ and /notes/[id])
components/
blocks/ # BlockCard, TextBlockBody, TodoBlockBody, TableBlockBody
ui/ # shadcn/ui components
lib/api.ts # Frontend API client → FastAPI
.env.local # NEXT_PUBLIC_API_BASE
Backend Setup (FastAPI + Postgres)
1. Create and activate virtualenv
From notes-app/backend:
bash
Copy code
# Windows PowerShell
python -m venv .venv
.\.venv\Scripts\activate
# Mac/Linux (for reference)
# python -m venv .venv
# source .venv/bin/activate
2. Install dependencies
bash
Copy code
pip install fastapi uvicorn[standard] sqlalchemy[asyncio] asyncpg python-dotenv
3. Environment variables
Create notes-app/backend/.env:
env
Copy code
DATABASE_URL=postgresql+asyncpg://<user>:<password>@<host>/<db_name>
DEV_USER_ID=00000000-0000-0000-0000-000000000001
DATABASE_URL should be your Neon (or other Postgres) connection string, but with postgresql+asyncpg://....
DEV_USER_ID is a fixed UUID used as a fake “current user” during development.
4. Create tables (one-time bootstrap)
In backend/main.py, there is a startup hook. For the first run, uncomment the table creation part:
python
Copy code
@app.on_event("startup")
async def on_startup():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
Then run:
bash
Copy code
uvicorn main:app --reload --port 8000
Once the app starts successfully and tables are created, comment that block back out so you don’t accidentally auto-mutate schema later.
5. Run backend
From notes-app/backend:
bash
Copy code
.\.venv\Scripts\activate # if not already active
uvicorn main:app --reload --port 8000
FastAPI docs: http://127.0.0.1:8000/docs
API root: http://127.0.0.1:8000
Frontend Setup (Next.js + shadcn/ui)
1. Install dependencies
From notes-app/frontend:
bash
Copy code
npm install
Make sure you’re using a reasonably recent Node LTS.
2. Frontend env
Create notes-app/frontend/.env.local:
env
Copy code
NEXT_PUBLIC_API_BASE=http://localhost:8000
This is used by lib/api.ts to talk to your FastAPI backend.
3. Run dev server
From notes-app/frontend:
bash
Copy code
npm run dev
App will be at:
http://localhost:3000
Frontend API Client (lib/api.ts)
The frontend talks to the backend via a small typed wrapper:
listNotes() → GET /notes
createNote(title) → POST /notes
deleteNote(id) → DELETE /notes/{id}
getNote(id) → GET /notes/{id}
createBlock(noteId, type, position, data) → POST /notes/{noteId}/blocks
updateBlock(blockId, data) → PATCH /blocks/{blockId}
deleteBlock(blockId) → DELETE /blocks/{blockId}
Block.data is a free-form JSON object where we store:
title – block title
collapsed – boolean
layout.size – "full" or "half"
Table-specific keys:
columns: string[]
rows: string[][]
Todo-specific keys:
items: { text: string; done: boolean }[]
Text-specific keys:
text: string
UI / Features
Dashboard (Home: /)
Shows list of notes.
Each note:
Title
Created/updated times (optional in UI)
Open button → /notes/[id]
Delete button → calls DELETE /notes/{id} with optimistic UI update.
“New note” button:
POST /notes
Backend also creates a default empty text block.
Note Page (/notes/[id])
Uses the loaded Note object from GET /notes/{id} which includes blocks: Block[].
Block Types
Text Block
Basic text area (or input) storing data.text.
Todo Block
List of todo items stored in data.items: { text, done }[].
Can add items, toggle them done/undone, and edit text.
Table Block
Uses data.columns and data.rows.
Columns:
Editable header names.
Add column button.
Delete column button (can’t delete the last column).
Rows:
Add row button.
Delete row icon per row.
Cells editable via <Input>.
Shared Block Behavior
Implemented in BlockCard:
Title per block
Editable Input bound to block.data.title.
Fallback label based on block type (Text/Todo/Table).
Collapse/Expand
data.collapsed boolean.
When collapsed, only header (title + controls) is shown.
Layout width
data.layout.size = "full" or "half".
Mapped to grid classes in the note page:
"full" → md:col-span-2
"half" → md:col-span-1
Delete block
onDelete calls DELETE /blocks/{id} with optimistic removal.
Performance / Saving
Editing a block uses optimistic updates + debounce:
Local state is updated immediately.
A setTimeout (~400ms) schedules a PATCH /blocks/{id}.
If more edits happen before the timer fires, the old timer is cleared, and a new one is scheduled.
This avoids spamming the backend on every keystroke.
API Overview (Backend)
Base URL: http://localhost:8000
Notes
GET /notes
Returns all notes for DEV_USER_ID. Each note can be returned without blocks or with blocks depending on your implementation; in this app, the dashboard usually only needs basic note metadata.
POST /notes
Creates a note and a default empty text block.
Request body:
json
Copy code
{ "title": "My note title" }
Response body:
json
Copy code
{
"id": "...",
"title": "My note title",
"created_at": "...",
"updated_at": "...",
"blocks": [
{
"id": "...",
"type": "text",
"position": 0,
"data": { "text": "" }
}
]
}
GET /notes/{note_id}
Returns a single note with its blocks.
DELETE /notes/{note_id}
Deletes the note and associated blocks (via explicit delete on blocks with note_id, or via ON DELETE CASCADE).
Blocks
POST /notes/{note_id}/blocks
Creates a block for a specific note. Frontend sends type, position, and initial data.
PATCH /blocks/{block_id}
Replaces the block’s data JSON with whatever the frontend sends, e.g.:
json
Copy code
{ "data": { "title": "Todos", "items": [] } }
DELETE /blocks/{block_id}
Deletes the block.
Development / Debugging Tips
Backend:
Watch the uvicorn console for stack traces.
500 errors usually mean DB issues (connection, schema, or async usage).
Frontend:
Check browser devtools console.
Check npm run dev terminal for TypeScript/Next.js errors.
If you change the DB schema in early development:
Easiest is often to DROP and re-run Base.metadata.create_all.
For a real project, you’d add Alembic migrations later.
Future Enhancements
Real user auth (instead of hard-coded DEV_USER_ID).
Additional block types (calendar, kanban, code snippet, embedded links).
Drag-and-drop block reordering.
Full-text search over note content.
Collaboration / sharing (multi-user notes).