Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TaskFlow

A simple full-stack task board (Board → Columns → Tasks), built for the TaskFlow take-home assignment.

Stack

  • Frontend: React 18 + JavaScript + Vite
  • Backend: Node.js + Express
  • Database: SQLite via better-sqlite3 (synchronous, real SQL — no ORM query builder hiding the queries)
  • Tests: Vitest + Supertest

Features

  • Board with To Do / In Progress / Done columns
  • Create, edit, and delete tasks
  • Move a task between columns via a dropdown (reliable over drag-and-drop, see Decisions below)
  • Low / Medium / High priority, set on create/edit
  • Filter visible tasks by priority
  • Task count shown in each column header
  • Backend validation: empty/whitespace-only titles are rejected with a 400, invalid priorities and invalid columns are rejected too
  • Frontend shows a real error banner (not a blank screen) if a request fails, and a "try again" state if the board itself can't load
  • Persistent SQLite database — reloading the page shows the same data
  • Seed data so the board isn't empty on first run
  • Two non-trivial SQL queries (see Database below)
  • Backend tests covering validation, moving a task, and a direct database-layer query

Prerequisites

  • Node.js 18 or newer (Node 20/22 recommended)
  • npm

Run locally (from a fresh clone)

# from the project root
npm install
npm run install:all
npm run dev

The frontend's dev server proxies /api/* requests to the backend (see client/vite.config.js), so there's no CORS setup to worry about and no hardcoded port baked into the UI.

The SQLite database file is created automatically at server/data/taskflow.db the first time the backend starts, and seeded with one board, three columns, and a few sample tasks if it's empty.

Running the two parts separately

npm run dev --prefix server   # backend only, http://localhost:5000
npm run dev --prefix client   # frontend only, http://localhost:5173

Tests

npm test

This runs the backend test suite (Vitest + Supertest) against a throwaway SQLite file (server/tests/test-taskflow.db), which is created fresh and deleted after each run. It covers:

  1. Creating a task with an empty/whitespace title fails with a 400.
  2. Moving a task to another column updates its column_id.
  3. The getTaskCountPerColumn database query returns the expected shape against known seed data.

Database

Schema: server/schema.sql. Applied automatically on backend startup (CREATE TABLE IF NOT EXISTS, so it's safe to re-run).

CREATE TABLE boards (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL
);

CREATE TABLE columns (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  board_id INTEGER NOT NULL,
  name TEXT NOT NULL,
  position INTEGER NOT NULL,
  FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
);

CREATE TABLE tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  column_id INTEGER NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  priority TEXT NOT NULL DEFAULT 'Medium' CHECK (priority IN ('Low', 'Medium', 'High')),
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
  FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
);

Foreign keys are enforced (PRAGMA foreign_keys = ON), and there are indexes on columns.board_id, tasks.column_id, and tasks.priority since those are the columns queried and joined on most often.

Seed data lives in server/src/db.js (seedDatabase) and only runs when the boards table is empty, so restarting the server never duplicates data.

The two required non-trivial queries (both in server/src/db.js, both real SQL — not "fetch everything and filter in JS"):

-- Task count per column on a board
SELECT c.id, c.name, COUNT(t.id) AS task_count
FROM columns c
LEFT JOIN tasks t ON t.column_id = c.id
WHERE c.board_id = ?
GROUP BY c.id, c.name
ORDER BY c.position;

-- Tasks with a given priority, newest first
SELECT t.id, t.title, t.description, t.priority, t.created_at, c.name AS column_name
FROM tasks t
JOIN columns c ON c.id = t.column_id
WHERE t.priority = ?
ORDER BY datetime(t.created_at) DESC, t.id DESC;

The second one is also exposed as an API endpoint (GET /api/tasks?priority=High) independent of the main board view, so it's genuinely queried from the database rather than derived from data already fetched for the board.

API

Method Route Description
GET /api/health Health check
GET /api/board The board with its columns and tasks
GET /api/tasks?priority=High Tasks of a given priority, newest first
GET /api/boards/:boardId/task-counts Task count per column
POST /api/tasks Create a task
PUT /api/tasks/:id Edit a task's title/description/priority
PATCH /api/tasks/:id/move Move a task to another column
DELETE /api/tasks/:id Delete a task

All mutating routes validate their input server-side (not just in the React form) and return a JSON { "error": "..." } body with an appropriate 4xx status on bad input.

Deploying (optional, but the assignment prioritizes it)

Simplest path — deploy the backend as a web service on Render/Railway/Fly.io:

  1. Point the service at the server/ directory, build command npm install, start command npm start.
  2. Note the deployed backend URL, e.g. https://taskflow-api.onrender.com.
  3. Deploy client/ as a static site (Vercel, Netlify, or the same host), setting the build command to npm run build, output directory dist, and an environment variable VITE_API_URL=https://taskflow-api.onrender.com/api.

SQLite is a file on disk, so on hosts with an ephemeral filesystem (like Render's free tier) the data resets on redeploy/restart — worth mentioning to reviewers, and fine for a take-home.

Decisions / assumptions

  • One default board, since multi-board/multi-team support and auth are explicitly out of scope.
  • Moving a task uses a dropdown rather than drag-and-drop, per the assignment's own guidance that a working dropdown beats a broken drag-and-drop within a time-boxed assignment.
  • The frontend calls a relative /api path, proxied to the backend by Vite in dev (client/vite.config.js). This avoids hardcoding localhost:5000 into the UI and sidesteps CORS entirely in local dev, while still allowing a split deployment via VITE_API_URL.
  • Timestamps are stored as ISO-8601 (YYYY-MM-DDTHH:MM:SS.sssZ) rather than SQLite's default CURRENT_TIMESTAMP format, so new Date(task.created_at) parses reliably in every browser (the default space-separated format isn't reliably parsed by Date outside Chrome).
  • SQLite via better-sqlite3 because the assignment explicitly allows it and its synchronous API keeps the query code simple and easy to read for a small project like this.

If I had more time

Drag-and-drop, title search, pagination for larger boards, optimistic UI updates on move/delete instead of a full board reload, and a persistent (non-ephemeral) deployment.

Approximate time spent

About 2–3 hours.

Interesting note

Storing created_at as SQLite's default CURRENT_TIMESTAMP looks harmless until you actually parse it with new Date(...) in a browser other than Chrome — the lack of a T/Z in that format isn't reliably parsed per the ECMAScript spec, so it's an easy bug to ship without ever noticing on one machine.

About

Full-stack task board application built with React and Node.js

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages