Create the project
mkdir tasks-api
cd tasks-api
bun init -y
bun add volttsThe 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.
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
bun run index.tsCreate and read a task
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/1POST 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
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/1PUT returns 200 with the updated task, DELETE returns 204, and the final GET returns 404 with {"error":"Task not found"}.
Check validation failures
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.