A minimal clone built in 45 minutes using Effect‑TS, Node 18+, and SQLite
Welcome to another official UVF IT guide! In this quick‑start you’ll build a tiny Todo application inspired by classic productivity tools, but powered by the functional powerhouse Effect‑TS. We’ll keep it lean, using only Node, TypeScript, and a local SQLite database – no paid services required.
The result is a fully runnable server exposing a tiny JSON API (GET/POST/DELETE) that you can extend into a full‑stack app. Follow the phases, copy the code blocks, and you’ll have a working clone in under an hour.
npm is available.@effect/sql-sqlite-node; on Windows you may need the sqlite3 binary which is bundled with the package.Create a fresh npm workspace and install Effect‑TS core plus the SQLite driver.
mkdir effect-todo && cd effect-todo
npm init -y
npm install typescript @effect/core @effect/sql-sqlite-node @effect/platform-node @effect/platform-http
npx tsc --init --rootDir src --outDir dist --esModuleInterop true --moduleResolution node --module commonjs --target es2022We model a Todo as a simple record and expose functions to list, add, and delete items using Effect‑TS’s Effect API.
src/repo.ts
import { Effect } from '@effect/core'
import * as Sql from '@effect/sql-sqlite-node'
import * as Schema from '@effect/schema/Schema'
// Simple Todo record
export interface Todo {
id: number
title: string
completed: boolean
}
// SQLite connection (singleton for the app)
const db = Sql.make({ filename: ':memory:' })
// Initialise table
const init = Sql.run(db, `CREATE TABLE IF NOT EXISTS todo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL CHECK (completed IN (0,1))
)`)
// List all todos
export const listTodos = Effect.gen(function* (_) {
yield* _(init) // ensure table exists
const rows = yield* _(Sql.all(db, 'SELECT * FROM todo', [], Schema.struct({
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean,
})))
return rows as Todo[]
})
// Add a new todo
export const addTodo = (title: string) =>
Effect.gen(function* (_) {
yield* _(init)
const result = yield* _(Sql.run(db, 'INSERT INTO todo (title, completed) VALUES (?, 0)', [title]))
const id = result.lastInsertRowid as number
return { id, title, completed: false } as Todo
})
// Delete a todo by id
export const deleteTodo = (id: number) =>
Effect.gen(function* (_) {
yield* _(init)
yield* _(Sql.run(db, 'DELETE FROM todo WHERE id = ?', [id]))
return undefined
})Effect‑TS ships a small but powerful HTTP module that works nicely with its functional runtime. In this step we wire the repository functions we created earlier to three REST endpoints: list all todos, create a new todo, and delete an existing one. Each handler returns an Effect that the server runs, keeping everything pure and type‑safe. By the end you’ll have a minimal JSON API listening on port 3000.
src/server.ts
import * as T from '@effect-ts/core/Effect'
import * as Http from '@effect-ts/http'
import * as Router from '@effect-ts/http/Router'
import * as Body from '@effect-ts/http/Body'
// Simple in‑memory store – replace with your real repo later
let todos: {id:number, title:string}[] = []
let nextId = 1
const getTodos = T.succeed(todos)
const addTodo = (title:string) => T.succeed(() => {
const todo = {id: nextId++, title}
todos.push(todo)
return todo
})
const deleteTodo = (id:number) => T.succeed(() => {
todos = todos.filter(t => t.id !== id)
})
const api = Router.make({
get: {
'/todos': Http.okJson(getTodos)
},
post: {
'/todos': Http.jsonBody(Body.json<{title:string}>())
.flatMap(body => addTodo(body.title))
.flatMap(Http.okJson)
},
delete: {
'/todos/:id': Http.param('id')
.flatMap(id => deleteTodo(Number(id)))
.flatMap(() => Http.noContent)
}
})
const server = Http.make({ port: 3000, host: '0.0.0.0' }, api)
T.runPromise(server)We don’t need a separate build step; ts-node can compile TypeScript on the fly and hand it to the Effect‑TS runtime. Just install the dev dependencies once and launch the server with a single command. The process will print a friendly listening message so you know it’s ready for requests.
npm install --save-dev ts-node @types/node
npx ts-node src/server.tsWith the server running, use curl or your browser to hit the three endpoints and watch the JSON responses. Start with a GET to see an empty list, POST a new todo, then GET again to confirm it was stored. You can also DELETE a todo and verify the list empties. From here you can replace the in‑memory store with SQLite, add validation, or build a front‑end.
# 1. Check empty list
curl http://localhost:3000/todos
# 2. Add a todo
curl -X POST -H 'Content-Type: application/json' -d '{"title":"Buy milk"}' http://localhost:3000/todos
# 3. Verify it was added
curl http://localhost:3000/todos
# 4. Delete the todo (id will be 1)
curl -X DELETE http://localhost:3000/todos/1
# 5. Confirm list is empty again
curl http://localhost:3000/todos| Check | Command / Why it matters |
|---|---|
| Project initializes without TypeScript errors | npx tsc --noEmit |
| Server starts and prints listening message | npx ts-node src/server.ts (look for ‘Listening on http://localhost:3000’) |
| GET /todos returns an empty JSON array | curl http://localhost:3000/todos | grep '\[\]' |
You now have a functional Effect‑TS backend. Consider adding validation with @effect/schema, a React front‑end, or persisting timestamps. Happy hacking!