Build a minimal clone of Doska using Node.js, Express, and SQLite
Welcome to another official UVF IT guide! In this quick‑start you’ll spin up a lightweight Kanban board that stores cards as Markdown files, inspired by the Doska project. We’ll use only free, open‑source tools so you can get a working prototype in under an hour.
We’ll scaffold a tiny Express app, define a SQLite schema for boards and cards, expose a simple JSON API, and serve a minimal HTML UI. The result is a functional clone you can run locally and extend as you like.
npm install --global windows-build-tools for the better-sqlite3 native module.Create a fresh directory for your board and initialise it as an npm project. This gives you a package.json to track dependencies and scripts. Then install Express for the web server and better‑sqlite3 for fast, synchronous SQLite access.
We also add a basic folder layout: src/ for server code and public/ for static assets. This keeps the project tidy and mirrors how many Node.js starters are organised.
mkdir doska-clone && cd doska-clone
npm init -y
npm install express@4 better-sqlite3@8
mkdir -p src publicOn the first run the server will create a cards.db file with a single cards table. Each card stores its markdown content, the column it belongs to (e.g., "todo", "doing", "done"), and timestamps for creation and updates. Using SQLite means we avoid a separate database server – everything lives in one file, perfect for a lightweight clone.
initDb.js
import Database from 'better-sqlite3';
// Open (or create) the SQLite file in the project root
const db = new Database('cards.db');
// Create the cards table if it doesn't exist yet
const createTable = `
CREATE TABLE IF NOT EXISTS cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
markdown TEXT NOT NULL,
column TEXT NOT NULL CHECK(column IN ('todo','doing','done')),
created_at DATETIME DEFAULT (datetime('now')),
updated_at DATETIME DEFAULT (datetime('now'))
);
`;
db.exec(createTable);
export default db;In this step we wire up an Express server that talks to a SQLite database. The server will expose three simple REST endpoints: one to fetch all cards, one to create a new card, and one to move a card between columns. It also serves a tiny static HTML page that uses fetch to call those endpoints, so you can see the board in your browser.
We keep the UI minimal: three columns (Todo, In‑Progress, Done) and a form to add a new card. When a card is moved, we send a PATCH request to update its column. This gives you a working Kanban board without any fancy front‑end frameworks.
server.js
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const app = express();
const db = new sqlite3.Database('kanban.db');
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Initialise DB
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
markdown TEXT,
column TEXT NOT NULL DEFAULT 'Todo'
)`);
});
// List all cards
app.get('/api/cards', (req, res) => {
db.all('SELECT * FROM cards', [], (err, rows) => {
if (err) return res.status(500).json({error: err.message});
res.json(rows);
});
});
// Create a new card
app.post('/api/cards', (req, res) => {
const {title, markdown} = req.body;
const stmt = db.prepare('INSERT INTO cards (title, markdown) VALUES (?, ?)');
stmt.run(title, markdown || '', function(err) {
if (err) return res.status(500).json({error: err.message});
res.json({id: this.lastID, title, markdown, column: 'Todo'});
});
});
// Move a card to a different column
app.patch('/api/cards/:id', (req, res) => {
const {column} = req.body;
const stmt = db.prepare('UPDATE cards SET column = ? WHERE id = ?');
stmt.run(column, req.params.id, function(err) {
if (err) return res.status(500).json({error: err.message});
res.json({updated: this.changes});
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 http://localhost:${PORT}`));Now that the server code and UI are in place, start the application with Node. The server will automatically serve the public folder, so opening the root URL loads the board. You can keep the terminal open to watch any runtime errors.
node server.jsOpen http://localhost:3000 in your browser. You should see three column headings and a form at the top. Create a card, watch it appear in the "Todo" column, then use the move buttons to shift it to "In‑Progress" or "Done". From here you can enhance the UI with markdown rendering (e.g., the marked library) and add drag‑and‑drop using SortableJS, or persist each card as a .md file for true Doska‑style editing.
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Minimal Kanban</title>
<style>
body {font-family: Arial, sans-serif; margin: 20px;}
.board {display: flex; gap: 10px;}
.column {flex: 1; border: 1px solid #ccc; padding: 10px;}
.card {background:#f9f9f9; margin:5px 0; padding:5px; border:1px solid #ddd;}
button {margin-left:5px;}
</style>
</head>
<body>
<h1>Kanban Board</h1>
<form id="newCardForm">
<input type="text" id="title" placeholder="Card title" required />
<input type="text" id="markdown" placeholder="Markdown (optional)" />
<button type="submit">Add Card</button>
</form>
<div class="board" id="board">
<div class="column" data-col="Todo"><h2>Todo</h2></div>
<div class="column" data-col="In-Progress"><h2>In‑Progress</h2></div>
<div class="column" data-col="Done"><h2>Done</h2></div>
</div>
<script>
async function loadCards(){
const res = await fetch('/api/cards');
const cards = await res.json();
document.querySelectorAll('.column').forEach(col=>col.innerHTML = `<h2>${col.dataset.col}</h2>`);
cards.forEach(c=>addCardToDOM(c));
}
function addCardToDOM(card){
const colDiv = document.querySelector(`.column[data-col="${card.column}"]`);
const cardDiv = document.createElement('div');
cardDiv.className='card';
cardDiv.textContent = card.title;
const moveBtn = document.createElement('button');
moveBtn.textContent='→';
moveBtn.onclick = async()=>{
const next = { 'Todo':'In-Progress','In-Progress':'Done','Done':'Todo' }[card.column];
await fetch(`/api/cards/${card.id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({column:next})});
loadCards();
};
cardDiv.appendChild(moveBtn);
colDiv.appendChild(cardDiv);
}
document.getElementById('newCardForm').addEventListener('submit', async e=>{
e.preventDefault();
const title = document.getElementById('title').value;
const markdown = document.getElementById('markdown').value;
await fetch('/api/cards',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title,markdown})});
document.getElementById('title').value='';
document.getElementById('markdown').value='';
loadCards();
});
loadCards();
</script>
</body>
</html>| Check | Command / Why it matters |
|---|---|
| Server starts without errors | node server.js → should log ‘🚀 http://localhost:3000’ |
| UI loads in browser | Open http://localhost:3000 → page with three column headings appears |
| Create a card via API | curl -X POST -H 'Content-Type: application/json' -d '{"title":"Test","markdown":"# Test"}' http://localhost:3000/api/cards → returns JSON with id |
You now have a functional markdown‑based Kanban clone. Consider adding a markdown renderer (e.g., marked), drag‑and‑drop with SortableJS, and persisting each card as a .md file for true Doska‑style editing.