Examples

The official Goddo demo (in src/ in the repository) is a full Todo API: CRUD with validation via t, automatic documentation with @goddo/openapi, an /llms.txt endpoint for AI agents, and a true HATEOAS / HTMX architecture with TSX Server-Side Rendering.

Standalone Demo: If you want to see an example using the published JSR packages, check out the goddo-example repository.

Entrypoint

src/index.ts is the file that actually starts the server: it imports the app instance defined in src/app.tsx and calls .listen() on it. Running deno task dev (or deno task start) executes this file:

import { app } from './app.tsx'

app.listen(3000)

API & Content Negotiation

The src/app.tsx file contains the API routes. Notice how Goddo easily handlesContent Negotiation: if the request comes from HTMX (checking the HX-Request header), it returns the rendered TSX component fragment. Otherwise, it functions as a standard JSON REST API.

import { Goddo, t } from '@goddo/core'
import { html } from '@goddo/html'
import { openapi } from '@goddo/openapi'
import { llmstxt } from '@goddo/llms-txt'
import { renderPage, type Todo } from './page.tsx'
import { TodoItem } from './components/TodoItem.tsx'

// Seed data — single source of truth for the initial store state
const SEED: [number, Todo][] = [
  [1, { id: 1, title: 'Buy groceries', completed: true }],
  [2, { id: 2, title: 'Walk the dog', completed: false }],
]

// In-memory store (Map = O(1) lookup/delete, analogous to a DB primary-key index)
const todos = new Map<number, Todo>(SEED)
let idCounter = SEED.length + 1

const todosArray = () => [...todos.values()]

/** Resets the store to its initial seed state. Used by benchmarks to avoid cross-benchmark pollution. */
export function resetStore() {
  todos.clear()
  for (const [k, v] of SEED) todos.set(k, { ...v })
  idCounter = SEED.length + 1
}

export const app = new Goddo()
  .use(
    openapi({
      documentation: {
        info: { title: 'Todo API', version: '1.0.0', description: 'A simple Todo CRUD API.' },
      },
    }),
  )
  .use(llmstxt({
    title: 'My Todos API',
    description: 'An API to manage your tasks',
  }))
  .use(html())
  .get('/', ({ redirect }) => redirect('/page'))
  .get('/page', () => renderPage(todosArray()))
  .group('/todos', (app) =>
    app
      .get('/', () => todosArray(), {
        detail: { summary: 'List all todos', tags: ['Todos'] },
      })
      .get('/:id', ({ params: { id }, set }) => {
        const todo = todos.get(id)
        if (!todo) {
          set.status = 404
          return { error: 'Todo not found' }
        }
        return todo
      }, {
        params: t.Object({ id: t.Numeric() }),
        detail: { summary: 'Get a todo by ID', tags: ['Todos'] },
      })
      .post('/', ({ body, headers }) => {
        const todo: Todo = { id: idCounter++, title: body.title, completed: false }
        todos.set(todo.id, todo)
        if (headers['hx-request']) return TodoItem(todo)
        return todo
      }, {
        body: t.Object({ title: t.String() }),
        detail: { summary: 'Create a new todo', tags: ['Todos'] },
      })
      .patch('/:id', ({ params: { id }, body, headers, set }) => {
        const todo = todos.get(id)
        if (!todo) {
          set.status = 404
          return { error: 'Todo not found' }
        }
        if (body.title !== undefined) todo.title = body.title
        if (body.completed !== undefined) todo.completed = body.completed
        if (headers['hx-request']) return TodoItem(todo)
        return todo
      }, {
        params: t.Object({ id: t.Numeric() }),
        body: t.Object({
          title: t.Optional(t.String()),
          completed: t.Optional(t.Boolean()),
        }),
        detail: { summary: 'Partially update a todo', tags: ['Todos'] },
      })
      .delete('/:id', ({ params: { id }, headers, set }) => {
        if (!todos.has(id)) {
          set.status = 404
          return { error: 'Todo not found' }
        }
        todos.delete(id)
        if (headers['hx-request']) return ''
        return { success: true }
      }, {
        params: t.Object({ id: t.Numeric() }),
        detail: { summary: 'Delete a todo', tags: ['Todos'] },
      }))

HATEOAS Interface (HTMX + Alpine.js)

The UI is split into components to enable hypermedia-driven interactions.src/page.tsx orchestrates the page:

import { Layout } from './components/Layout.tsx'
import { TodoItem } from './components/TodoItem.tsx'

export type Todo = { id: number; title: string; completed: boolean }

export function renderPage(todos: Todo[]) {
  return (
    <Layout>
      <main x-data>
        <article style='margin-top: 3rem;'>
          <h1 style='display: flex; align-items: center; gap: 0.5rem;'>
            <i class='ph ph-list-checks' style='color: var(--pico-primary);'></i> My Todos
          </h1>

          <form
            x-data="{ title: '' }"
            hx-post='/todos'
            hx-ext='json-enc'
            hx-target='#todo-list'
            hx-swap='beforeend'
            {...{
              'x-on:htmx:after-request': 'if($event.detail.successful) title = ""',
            }}
          >
            <fieldset role='group'>
              <input
                type='text'
                name='title'
                x-model='title'
                placeholder='What needs to be done?'
                required
              />
              <button
                type='submit'
                style='display: flex; align-items: center; justify-content: center; gap: 0.25rem;'
              >
                <i class='ph ph-plus-circle'></i> Add
              </button>
            </fieldset>
          </form>

          <table>
            <tbody id='todo-list'>
              {todos.map((todo) => <TodoItem key={todo.id} {...todo} />)}
            </tbody>
          </table>
        </article>

        <article style='margin-top: 1rem; background-color: var(--pico-form-element-background); text-align: center; padding: 2rem;'>
          <h3 style='margin-bottom: 0.5rem; display: flex; align-items: center; justify-content: center; gap: 0.5rem;'>
            <i class='ph ph-book-open-text' style='color: var(--pico-primary);'></i>{' '}
            API Documentation
          </h3>
          <p style='margin-bottom: 1.5rem; color: var(--pico-muted-color);'>
            Want to see the API that powers this app? Check out the auto-generated interactive
            documentation.
          </p>
          <a
            href='/docs'
            role='button'
            class='secondary'
            style='display: inline-flex; align-items: center; gap: 0.5rem;'
          >
            View API Docs <i class='ph ph-arrow-right'></i>
          </a>
        </article>
      </main>
    </Layout>
  )
}

src/components/TodoItem.tsx is a standalone component that replaces itself in the DOM via hx-swap="outerHTML" when toggled, edited, or deleted:

import type { Todo } from '../page.tsx'

export function TodoItem(todo: Todo & { key?: string | number }) {
  // We use hx-swap="outerHTML" to replace the entire <tr> element with the new one
  // hx-ext="json-enc" is used to keep sending JSON to match the strict type schemas
  return (
    <tr id={`todo-${todo.id}`}>
      <td width='100%'>
        <label>
          <input
            type='checkbox'
            checked={todo.completed}
            hx-patch={`/todos/${todo.id}`}
            hx-ext='json-enc'
            hx-vals={JSON.stringify({ completed: !todo.completed })}
            hx-target={`#todo-${todo.id}`}
            hx-swap='outerHTML'
          />
          {todo.completed ? <s>{todo.title}</s> : todo.title}
        </label>
      </td>
      <td align='right' style='vertical-align: middle;'>
        <div style='display: flex; gap: 0.5rem; justify-content: flex-end;'>
          <button
            type='button'
            x-data={`{ currentTitle: '${todo.title.replace(/'/g, "\\'")}' }`}
            hx-patch={`/todos/${todo.id}`}
            hx-ext='json-enc'
            hx-target={`#todo-${todo.id}`}
            hx-swap='outerHTML'
            {...{
              'x-on:htmx:config-request':
                "let newTitle = prompt('Edit todo:', currentTitle); if (newTitle !== null && newTitle.trim() !== '') { $event.detail.parameters.title = newTitle.trim(); } else { $event.preventDefault(); }",
            }}
            style='margin-bottom: 0; min-width: 90px; display: flex; align-items: center; justify-content: center; gap: 0.25rem; background-color: var(--pico-secondary-background); border-color: var(--pico-secondary-border);'
          >
            <i class='ph ph-pencil-simple'></i> Edit
          </button>
          <button
            type='button'
            hx-delete={`/todos/${todo.id}`}
            hx-target={`#todo-${todo.id}`}
            hx-swap='outerHTML'
            style='margin-bottom: 0; min-width: 90px; display: flex; align-items: center; justify-content: center; gap: 0.25rem;'
          >
            <i class='ph ph-trash'></i> Delete
          </button>
        </div>
      </td>
    </tr>
  )
}

src/components/Layout.tsx encapsulates the document boilerplate:

import { HtmlString } from '@goddo/html'

export function Layout({ children }: { children: unknown }) {
  const page = (
    <html lang='en'>
      <head>
        <meta charset='utf-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1' />
        <title>My Todos</title>
        <link
          rel='stylesheet'
          href='https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css'
        />
        <script src='https://unpkg.com/@phosphor-icons/web'></script>
        <script defer src='https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js'></script>
        <script src='https://unpkg.com/htmx.org@1.9.12'></script>
        <script src='https://unpkg.com/htmx.org/dist/ext/json-enc.js'></script>
        <style>
          {`html { font-size: 14px; }`}
        </style>
      </head>
      <body>
        {children}
      </body>
    </html>
  )
  return new HtmlString('<!DOCTYPE html>\n' + page)
}

Running the demo locally

git clone https://github.com/carlosxfelipe/goddo.git
cd goddo
deno task dev

The demo runs at http://localhost:3000. The UI is at /page, the OpenAPI documentation at /docs and the endpoint for LLMs at /llms.txt.

Bruno & Benchmarks

The repository also includes API collections for Bruno (in the bruno/ folder) and a set of performance benchmarks (deno task bench) covering routing, compiled handler throughput, and compilation overhead.