Project Setup
The domain
A file sharing app: users upload files, get download links, manage their uploads, and share files with others. Simple enough to learn from, complex enough to demonstrate every upload pattern.
Create the project
mkdir fileshare-api
cd fileshare-api
npm init -y
npm install @hectoday/http zod srvx better-sqlite3 busboy sharp mime-types
npm install -D typescript @types/node @types/better-sqlite3 @types/busboy @types/mime-types tsx New dependencies:
busboy: Streaming multipart form-data parser. Processes uploads chunk by chunk without loading the entire file into memory.
sharp: High-performance image processing. Resize, crop, generate thumbnails.
mime-types: Maps file extensions to MIME types and vice versa.
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
} The database schema
// src/db.ts
import Database from "better-sqlite3";
const db = new Database("fileshare.db");
db.pragma("journal_mode = WAL");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
original_name TEXT NOT NULL,
stored_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size INTEGER NOT NULL,
is_public INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id)
);
`);
// Seed a test user
const existing = db.prepare("SELECT id FROM users LIMIT 1").get();
if (!existing) {
db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)").run(
"user-alice",
"Alice",
"[email protected]",
);
}
export default db; The files table tracks metadata: original_name (what the user uploaded), stored_name (the unique name on disk), mime_type, size (in bytes), and is_public (whether anyone can download it).
The actual file bytes are stored on disk in an uploads/ directory, not in the database. Databases are for structured data; file systems are for files.
The uploads directory
// src/storage.ts
import { mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
const UPLOAD_DIR = join(process.cwd(), "uploads");
const THUMB_DIR = join(process.cwd(), "uploads", "thumbnails");
if (!existsSync(UPLOAD_DIR)) mkdirSync(UPLOAD_DIR, { recursive: true });
if (!existsSync(THUMB_DIR)) mkdirSync(THUMB_DIR, { recursive: true });
export { UPLOAD_DIR, THUMB_DIR }; App shell
// src/app.ts
import { setup, route } from "@hectoday/http";
export const app = setup({
routes: [route.get("/health", { resolve: () => Response.json({ status: "ok" }) })],
}); // src/server.ts
import { serve } from "srvx";
import { app } from "./app.js";
serve({ fetch: app.fetch, port: 3000 }); Add "type": "module" and "scripts": { "dev": "tsx watch src/server.ts" } to package.json.
Exercises
Exercise 1: Start the server and verify the uploads/ and uploads/thumbnails/ directories are created.
Exercise 2: Look at the files table. Why do we store original_name and stored_name separately? (Answer: the original name might contain path traversal characters or duplicates. The stored name is a safe, unique identifier.)
Why are uploaded files stored on disk instead of in the database?