Weekend Build Part 2

Backend: API & Database

Saturday afternoon. Architecture validated, types defined, milestones planned. Now we build the entire backend — database, auth, and all API endpoints — in one focused session.


M1Project Scaffolding

Scaffold the Monorepo

We need two projects — /client and /server — in a single repository. AI sets this up in one prompt.

You

Scaffold a monorepo for Taskflow with this structure:

/server — Express + TypeScript. Use tsx for dev mode (hot reload). Port 3001.
/client — Vite + React 18 + TypeScript + Tailwind. Port 5173.
/shared — Shared TypeScript types (the types I defined in Part 1).

Give me: package.json files for root and each workspace, tsconfig files, and the exact terminal commands to set everything up.

AI will generate the full scaffolding. Here's what the project structure looks like after setup:

taskflow/
+-- package.json → workspaces: ["client", "server", "shared"]
+-- shared/
— +-- package.json
— +-- types.ts → our types from Part 1
+-- server/
— +-- package.json
— +-- tsconfig.json
— +-- src/
— +-- index.ts → Express app entry
— +-- routes/
— +-- middleware/
— +-- db/
+-- client/
    +-- package.json
    +-- vite.config.ts
    +-- tailwind.config.js
    +-- src/

The verification step: both projects should run simultaneously and the server should respond to a health check.

$ cd taskflow && npm install
$ npm run dev
-- server running on http://localhost:3001
-- client running on http://localhost:5173

$ curl http://localhost:3001/health
{"status":"ok","timestamp":"2026-02-15T10:30:00Z"}
M1 Complete — Both projects running with hot reload

M2Database & Models

Build the Database Layer

SQLite with better-sqlite3 — synchronous, zero-config, perfect for a weekend build. Notice how we share the types from Part 1 as context.

You

Here are the shared types for Taskflow:

[paste types.ts from Part 1]

Create the SQLite database layer using better-sqlite3.

I need:
1. Schema migration that creates users, projects, and tasks tables
2. A db.ts module that initializes the database and exports the connection
3. Use parameterized queries everywhere (never string interpolation)
4. Use UUIDs for IDs (use crypto.randomUUID())
5. Add indexes on foreign keys and commonly filtered columns (status, priority)

The key output is the schema migration. Here's what it should look like:

CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,
  name TEXT NOT NULL,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS projects (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  owner_id TEXT NOT NULL REFERENCES users(id),
  archived INTEGER DEFAULT 0,
  created_at TEXT DEFAULT (datetime('now'))
);

CREATE TABLE IF NOT EXISTS tasks (
  id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL REFERENCES projects(id),
  title TEXT NOT NULL,
  description TEXT DEFAULT '',
  status TEXT DEFAULT 'todo'
    CHECK (status IN ('todo','in_progress','review','done')),
  priority TEXT DEFAULT 'medium'
    CHECK (priority IN ('low','medium','high','urgent')),
  assignee TEXT,
  due_date TEXT,
  position REAL DEFAULT 0,
  completed_at TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
CREATE INDEX IF NOT EXISTS idx_projects_owner ON projects(owner_id);

Pro Tip: REAL for Position

Notice position REAL instead of INTEGER. This is the critique fix from Part 1. When you drag a task between two others, you just calculate the midpoint: insert between position 1.0 and 2.0 → position 1.5. No need to renumber every task in the column.

Verification: run the migration and confirm the tables exist.

$ curl http://localhost:3001/health
{"status":"ok","db":"connected","tables":["users","projects","tasks"]}
M2 Complete — Database initialized with all tables and indexes

M3Authentication

Build Authentication

JWT-based auth with bcrypt password hashing. This is where security matters most — and where AI's defaults are most dangerous. We specify security requirements explicitly.

You

Build the auth system for Taskflow.

Here's the existing db module: [paste db.ts]
Here are the types: [paste types.ts]

I need:
1. POST /auth/register — email, password, name. Hash password with bcrypt (cost 12). Return JWT.
2. POST /auth/login — email, password. Verify, return JWT with 24h expiration.
3. authMiddleware — Verify JWT from Authorization header. Attach user to req.user.

Security requirements:
- Validate email format and password length (min 8 chars)
- Generic error messages: don't reveal if email exists
- Use process.env.JWT_SECRET (never hardcode)
- Parameterized queries only
- Rate limiting is out of scope for this weekend

AI generates the auth routes and middleware. The critical things to verify in the output:

Always Check Auth Code

Auth is the one place where you read every line AI generates. Check for: hardcoded secrets, plain-text password storage, SQL injection, and missing validation. AI gets auth wrong more often than anything else. This 5-minute review prevents the worst category of security vulnerabilities.

Verification: test the full auth flow with curl.

$ curl -X POST http://localhost:3001/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"test@demo.com","password":"password123","name":"Test User"}'
{"token":"eyJhbG...","user":{"id":"abc-123","email":"test@demo.com","name":"Test User"}}

$ curl -X POST http://localhost:3001/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"test@demo.com","password":"password123"}'
{"token":"eyJhbG...","user":{"id":"abc-123","email":"test@demo.com","name":"Test User"}}

$ curl http://localhost:3001/projects \
  -H "Authorization: Bearer eyJhbG..."
{"projects":[]} → authenticated, empty list

$ curl http://localhost:3001/projects
{"error":"No token provided","code":"AUTH_REQUIRED"} → no token, rejected
M3 Complete — Register, login, and JWT middleware all working

M4Project CRUD API

Project Endpoints

You

Build the project CRUD routes for Taskflow.

Here's the existing code: [paste db.ts, authMiddleware.ts, types.ts]

Endpoints:
- GET /projects — List all non-archived projects for the authenticated user
- POST /projects — Create project with name. Owner is the authenticated user.
- PATCH /projects/:id — Update name and/or archived status. Only owner can modify.
- DELETE /projects/:id — Actually just archives (set archived=true). Only owner.

All routes require authentication. Follow existing patterns in the auth routes. Return proper HTTP status codes (201 for create, 404 for not found, 403 for unauthorized).

Notice the pattern: we always paste existing code so AI matches the style. We specify the behavior precisely. And we reference the earlier critique — DELETE actually archives.

// GET /projects — only the user's non-archived projects
router.get('/', authMiddleware, (req, res) => {
  const projects = db
    .prepare('SELECT * FROM projects WHERE owner_id = ? AND archived = 0 ORDER BY created_at DESC')
    .all(req.user.id);

  res.json({ projects });
});

// DELETE /projects/:id — soft delete (archive)
router.delete('/:id', authMiddleware, (req, res) => {
  const project = db
    .prepare('SELECT * FROM projects WHERE id = ? AND owner_id = ?')
    .get(req.params.id, req.user.id);

  if (!project) return res.status(404).json({ error: 'Project not found', code: 'NOT_FOUND' });

  db.prepare('UPDATE projects SET archived = 1 WHERE id = ?').run(req.params.id);
  res.json({ success: true });
});
M4 Complete — All project endpoints working, tested with curl

M5Task CRUD API

Task Endpoints

The most complex set of endpoints. Tasks have more fields, more validation, and the kanban move operation requires special logic for position calculation.

You

Build the task CRUD routes. Here's all existing code: [paste everything]

Endpoints:
- GET /projects/:projectId/tasks — All tasks for a project, ordered by position within each status
- POST /projects/:projectId/tasks — Create task. Auto-assign position (append to end of 'todo' column)
- PATCH /tasks/:id — Update any task field. If status changes to 'done', set completed_at. Always update updated_at.
- DELETE /tasks/:id — Hard delete (tasks are cheap, no soft-delete needed)
- PATCH /tasks/:id/move — Move to new status and/or new position (for drag-and-drop). Body: { status, position }

Critical: the move endpoint must handle position correctly:
- Use float positions
- When moving to end of column, position = max existing position + 1
- When moving between two tasks, position = average of their positions
- Verify the project belongs to the authenticated user before any operation

The move endpoint is the trickiest part. Here's the core logic:

router.patch('/:id/move', authMiddleware, (req, res) => {
  const { status, position } = req.body as MoveTaskInput;
  const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);

  if (!task) return res.status(404).json({ error: 'Task not found', code: 'NOT_FOUND' });

  // Verify ownership through project
  const project = db
    .prepare('SELECT * FROM projects WHERE id = ? AND owner_id = ?')
    .get(task.project_id, req.user.id);

  if (!project) return res.status(403).json({ error: 'Forbidden', code: 'FORBIDDEN' });

  const updates: Record<string, unknown> = {
    status,
    position,
    updated_at: new Date().toISOString(),
  };

  // If moving to 'done', record completion time
  if (status === 'done' && task.status !== 'done') {
    updates.completed_at = new Date().toISOString();
  }

  // If moving away from 'done', clear completion time
  if (status !== 'done' && task.status === 'done') {
    updates.completed_at = null;
  }

  db.prepare(
    'UPDATE tasks SET status = ?, position = ?, updated_at = ?, completed_at = ? WHERE id = ?'
  ).run(updates.status, updates.position, updates.updated_at, updates.completed_at, req.params.id);

  const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(req.params.id);
  res.json({ task: updated });
});

Now: Adversarial Review

With all five milestones of the backend complete, we run the adversarial review from the manual before moving on.

You

You just wrote all the backend code for Taskflow. Now switch roles: you're a hostile code reviewer. Find every bug, security hole, and design flaw. Be ruthless.

[paste all route files]

AI

Issues found:

1. No input validation on task creation — title could be empty string. Add: if (!title?.trim()) check.

2. Position could be NaN — If frontend sends bad data to move endpoint. Add: if (typeof position !== 'number' || isNaN(position)) guard.

3. Missing CORS — You mentioned it in the critique but haven't added it. Add cors({ origin: 'http://localhost:5173' }) to Express.

4. No global error handler — If any route throws, Express returns a stack trace. Add error-handling middleware that catches unhandled errors and returns a clean JSON response.

5. SQL injection is clean — All queries use parameterized statements. No issues found.

6. Auth looks solid — bcrypt cost 12, JWT from env, generic error messages.

Four real issues found. Fix all of them before moving to the frontend. This 2-minute review just prevented four bugs that would have appeared in production.

M5 Complete — All task endpoints + adversarial review fixes applied

End of Saturday: What We Have

The entire backend is built, reviewed, and working. Let's verify everything one more time with a comprehensive test sequence:

# Register
$ curl -s -X POST :3001/auth/register -H "Content-Type: application/json" \
  -d '{"email":"dev@taskflow.app","password":"weekend123","name":"Dev"}' | jq .token
"eyJhbG..."

# Create project
$ curl -s -X POST :3001/projects -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"name":"Weekend Build"}' | jq .project.id
"proj-abc-123"

# Create tasks
$ curl -s -X POST :3001/projects/proj-abc-123/tasks -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"title":"Build frontend","priority":"high"}' | jq .task.status
"todo"

# Move task to in_progress
$ curl -s -X PATCH :3001/tasks/task-xyz/move -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"status":"in_progress","position":1.0}' | jq .task.status
"in_progress"
Weekend Progress Check

One Saturday afternoon: a complete REST API with 11 endpoints, JWT authentication, input validation, parameterized SQL queries, proper error handling, and an adversarial code review. All built AI-first. Tomorrow we build the frontend.


Part 2 Complete — Backend Done

Previous Part Part 1 — Planning & Architecture
Next Part Part 3 — Frontend: Core UI