Volt v0.1 · TypeScript APIs on Bun · Get started
Docs/Getting Started/Tutorial: Build a Tasks API

Tutorial: Build a Tasks API

A complete CRUD API with validation, expected responses, and tests.

Create the project

bash
mkdir tasks-api
cd tasks-api
bun init -y
bun add voltts

The tutorial uses an in-memory Map. Restarting the process clears tasks. IDs increase independently of task count, so deleting one task does not reuse an existing ID.

Complete application

Save the entire file as index.ts. POST and PUT share a schema; both require title and done. PUT replaces the complete task input. DELETE returns an empty 204 response.

index.ts
import { Volt, t, type Static } from 'voltts';

const TaskInput = t.Object({
  title: t.String({ minLength: 2, maxLength: 120 }),
  done: t.Boolean(),
});
type Task = Static<typeof TaskInput> & { id: number };

export function createApp() {
  const app = new Volt({ port: 3000 });
  const tasks = new Map<number, Task>();
  let nextId = 1;

  app.get('/tasks', () => ({ tasks: [...tasks.values()] }));
  app.get('/tasks/:id', (c) => {
    const task = tasks.get(Number(c.params.id));
    return task ? c.json(task) : c.json({ error: 'Task not found' }, 404);
  });
  app.post('/tasks', (c) => {
    const input: Static<typeof TaskInput> = c.body;
    const task: Task = { id: nextId++, title: input.title, done: input.done };
    tasks.set(task.id, task);
    return c.json(task, 201);
  }, { body: TaskInput });
  app.put('/tasks/:id', (c) => {
    const id = Number(c.params.id);
    if (!tasks.has(id)) return c.json({ error: 'Task not found' }, 404);
    const input: Static<typeof TaskInput> = c.body;
    const task: Task = { id, title: input.title, done: input.done };
    tasks.set(id, task);
    return c.json(task);
  }, { body: TaskInput });
  app.delete('/tasks/:id', (c) => {
    if (!tasks.delete(Number(c.params.id))) {
      return c.json({ error: 'Task not found' }, 404);
    }
    return new Response(null, { status: 204 });
  });
  return app;
}

const app = createApp();
if (globalThis._importMeta_.main) app.listen(3000);
export default app;

Start the server

bash
bun run index.ts

Create and read a task

bash
curl -i -X POST http://localhost:3000/tasks \
  -H 'Content-Type: application/json' \
  -d '{"title":"Learn Volt","done":false}'
curl -i http://localhost:3000/tasks
curl -i http://localhost:3000/tasks/1

POST returns 201 with {"id":1,"title":"Learn Volt","done":false}. The list endpoint returns {"tasks":[...]}; reading ID 1 returns the task directly.

Update and delete

bash
curl -i -X PUT http://localhost:3000/tasks/1 \
  -H 'Content-Type: application/json' \
  -d '{"title":"Build a Volt API","done":true}'
curl -i -X DELETE http://localhost:3000/tasks/1
curl -i http://localhost:3000/tasks/1

PUT returns 200 with the updated task, DELETE returns 204, and the final GET returns 404 with {"error":"Task not found"}.

Check validation failures

bash
curl -i -X POST http://localhost:3000/tasks \
  -H 'Content-Type: application/json' -d '{"title":"x","done":false}'

Expect HTTP 400. The response contains a message and field details. The handler does not create a task when validation fails.

Add automated tests

Continue with Testing, which imports createApp from this file. Add OpenAPI for an interactive reference, then replace the Map with a real database using the Prisma guide.