Example: Todo API
A complete REST API for managing todos using Yama, PostgreSQL, and Fastify.
Overview
This example demonstrates:
- CRUD operations (Create, Read, Update, Delete)
- Database migrations
- Request validation
- API testing
Project Setup
Clone and run the example:
git clone https://github.com/BetagorsLabs/yama
cd yama/examples/todo-api
pnpm install
pnpm devThe API will be available at http://localhost:4000.
Configuration
yama.yaml
name: todo-api
version: 1.0.0
database:
url: ${DATABASE_URL}
schemas:
Todo:
type: object
required:
- title
properties:
id:
type: string
format: uuid
title:
type: string
minLength: 1
maxLength: 200
completed:
type: boolean
default: false
createdAt:
type: string
format: date-time
CreateTodoInput:
type: object
required:
- title
properties:
title:
type: string
completed:
type: boolean
UpdateTodoInput:
type: object
properties:
title:
type: string
completed:
type: boolean
endpoints:
/todos:
get:
handler: handlers/listTodos
summary: List all todos
response:
type: array
items:
$ref: "#/schemas/Todo"
post:
handler: handlers/createTodo
summary: Create a new todo
request:
$ref: "#/schemas/CreateTodoInput"
response:
$ref: "#/schemas/Todo"
/todos/{id}:
get:
handler: handlers/getTodo
summary: Get a todo by ID
response:
$ref: "#/schemas/Todo"
put:
handler: handlers/updateTodo
summary: Update a todo
request:
$ref: "#/schemas/UpdateTodoInput"
response:
$ref: "#/schemas/Todo"
delete:
handler: handlers/deleteTodo
summary: Delete a todo
response:
type: object
properties:
success:
type: booleanHandlers
List Todos
// src/handlers/listTodos.ts
import { HandlerContext } from '@betagors/yama-core';
export async function listTodos(context: HandlerContext) {
const todos = await context.db.query(
'SELECT * FROM todos ORDER BY created_at DESC'
);
return todos;
}Create Todo
// src/handlers/createTodo.ts
import { HandlerContext } from '@betagors/yama-core';
export async function createTodo(context: HandlerContext) {
const { title, completed = false } = context.request.body;
const [todo] = await context.db.query(
`INSERT INTO todos (title, completed)
VALUES ($1, $2)
RETURNING *`,
[title, completed]
);
context.reply.code(201);
return todo;
}Get Todo
// src/handlers/getTodo.ts
import { HandlerContext } from '@betagors/yama-core';
export async function getTodo(context: HandlerContext) {
const { id } = context.params;
const [todo] = await context.db.query(
'SELECT * FROM todos WHERE id = $1',
[id]
);
if (!todo) {
context.reply.code(404);
return { error: 'Todo not found' };
}
return todo;
}Update Todo
// src/handlers/updateTodo.ts
import { HandlerContext } from '@betagors/yama-core';
export async function updateTodo(context: HandlerContext) {
const { id } = context.params;
const { title, completed } = context.request.body;
const [todo] = await context.db.query(
`UPDATE todos
SET title = COALESCE($1, title),
completed = COALESCE($2, completed)
WHERE id = $3
RETURNING *`,
[title, completed, id]
);
if (!todo) {
context.reply.code(404);
return { error: 'Todo not found' };
}
return todo;
}Delete Todo
// src/handlers/deleteTodo.ts
import { HandlerContext } from '@betagors/yama-core';
export async function deleteTodo(context: HandlerContext) {
const { id } = context.params;
const result = await context.db.query(
'DELETE FROM todos WHERE id = $1 RETURNING id',
[id]
);
if (!result.length) {
context.reply.code(404);
return { error: 'Todo not found' };
}
return { success: true };
}Database Migration
Create the todos table:
-- migrations/001_create_todos.sql
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(200) NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_todos_created_at ON todos(created_at DESC);Apply the migration:
yama schema:applyTesting
Run the tests:
pnpm testExample Test
import { describe, it, expect } from 'vitest';
import { createTestClient } from '@betagors/yama-test';
describe('Todo API', () => {
const client = createTestClient();
it('should create a todo', async () => {
const response = await client.post('/todos', {
title: 'Learn Yama',
});
expect(response.status).toBe(201);
expect(response.data.title).toBe('Learn Yama');
expect(response.data.completed).toBe(false);
});
it('should list todos', async () => {
const response = await client.get('/todos');
expect(response.status).toBe(200);
expect(Array.isArray(response.data)).toBe(true);
});
});API Usage
Create a Todo
curl -X POST http://localhost:4000/todos \
-H "Content-Type: application/json" \
-d '{"title": "Learn Yama"}'List Todos
curl http://localhost:4000/todosUpdate a Todo
curl -X PUT http://localhost:4000/todos/{id} \
-H "Content-Type: application/json" \
-d '{"completed": true}'Delete a Todo
curl -X DELETE http://localhost:4000/todos/{id}Source Code
View the complete source code on GitHubĀ .
Last updated on