Skip to Content
šŸš€ Alpha Release - Yama JS is currently in alpha. APIs may change without notice.

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 dev

The 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: boolean

Handlers

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:apply

Testing

Run the tests:

pnpm test

Example 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/todos

Update 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