Production‑Ready Todo App with Effect‑TS

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.

Before you start…

Minimal Todo Clone Architecture
LOCAL STACK handles requests SQL queries Effect‑TS Todo Server Node + TypeScript HTTP Router SQLite DB
1

Phase 1: Scaffold the project

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 es2022
2

Phase 2: Define the Todo model and repository

We 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
  })
3

Phase 3: Build a tiny HTTP API with Effect‑TS

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)
4

Phase 4: Run locally

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.ts
5

Phase 5: Verify and extend

With 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

Verifying the Stack

CheckCommand / Why it matters
Project initializes without TypeScript errorsnpx tsc --noEmit
Server starts and prints listening messagenpx ts-node src/server.ts (look for ‘Listening on http://localhost:3000’)
GET /todos returns an empty JSON arraycurl http://localhost:3000/todos | grep '\[\]'

What’s next?

You now have a functional Effect‑TS backend. Consider adding validation with @effect/schema, a React front‑end, or persisting timestamps. Happy hacking!