Documentation

Complete Goddo API reference, with practical examples for every feature of the core (@goddo/core).

Syntax (Elysia-compatible)

new Goddo()
  .state('version', '1.0') // shared state
  .decorate('logger', console.log) // decorates the context
  .onRequest(({ path }) => console.log(path)) // lifecycle hook
  .get('/', () => 'text') // text/plain
  .get('/json', () => ({ ok: true })) // application/json (auto)
  .get('/user/:id', ({ params: { id } }) => id) // route params
  .get('/files/*', ({ params }) => params['*']) // wildcard
  .get('/q', ({ query }) => query) // query string
  .post('/body', ({ body }) => body) // automatic body parse
  .get('/error', ({ error }) => {
    throw error(418)
  })
  .get('/go', ({ redirect }) => redirect('/'))
  .group('/api', (app) => app.get('/health', () => 'ok'))
  .use(plugin) // plugin composition
  .onError(({ code }) => code === 'NOT_FOUND' ? 'Not found' : 'Error')
  .listen(3000)

Validation (t module)

TypeBox-style schemas for body, query, params, headers, and response, featuring end-to-end type inference in the handler and automatic coercion in query/params/headers. Invalid inputs return 422 (error code VALIDATION in onError).

import { Goddo, t } from '@goddo/core'

new Goddo()
  .post('/user', ({ body }) => body.name, { // body: { name: string; age: number }
    body: t.Object({ name: t.String(), age: t.Number() }),
  })
  .get('/user/:id', ({ params: { id } }) => id * 2, { // id: number (coerced from URL)
    params: t.Object({ id: t.Numeric() }),
  })
  .get('/list', ({ query }) => query, {
    query: t.Object({
      page: t.Numeric({ default: 1 }),
      active: t.Optional(t.Boolean()),
    }),
  })
  .listen(3000)

Available builders: t.String (minLength, maxLength, pattern, format), t.Number/t.Integer (minimum, maximum, multipleOf), t.Numeric (number with string coercion), t.Boolean, t.Null, t.Any, t.Unknown, t.Literal, t.Union, t.Enum, t.Nullable, t.Array (minItems, maxItems), t.Object (additionalProperties),t.Optional. All of them accept custom error messages and a default value.

Cookies

Reactive proxy-based cookies — read, set, and remove cookies directly on the cookie context object, matching Elysia's API:

new Goddo()
  .get('/visit', ({ cookie }) => {
    const count = Number(cookie.visits.value ?? 0)
    cookie.visits.value = String(count + 1)
    cookie.visits.set({ httpOnly: true, path: '/', maxAge: 86400 })
    return `Visits: ${count + 1}`
  })
  .get('/logout', ({ cookie }) => {
    cookie.session.remove()
    return 'Logged out'
  })
  .listen(3000)

Each cookie.<name> returns a Cookie object with .value, .set(attrs), .remove() and individual attribute setters (.httpOnly, .path, .sameSite, .secure, .maxAge, .domain, .expires, .priority).

Cookie schemas can be validated using t:

.get('/profile', ({ cookie }) => cookie.session.value, {
  cookie: t.Object({ session: t.String({ minLength: 1 }) }),
})

Signed Cookies

Cookies can be automatically signed and verified using the native Web Crypto API (HMAC-SHA256) by setting a cookieSecret in the GoddoConfig:

const app = new Goddo({ cookieSecret: 'my-super-secret' })
  .get('/sign', async ({ cookie }) => {
    cookie.auth.value = 'user_id_123'
    await cookie.auth.sign()
    return 'Cookie signed!'
  })
  .get('/verify', async ({ cookie }) => {
    const isValid = await cookie.auth.verify()
    return isValid ? `Hello ${cookie.auth.value}` : 'Invalid signature'
  })

Guard

Apply shared hooks and schemas to a group of routes:

new Goddo()
  .guard(
    {
      headers: t.Object({ authorization: t.String() }),
      beforeHandle: ({ headers, error }) => {
        if (!headers.authorization.startsWith('Bearer ')) throw error(401)
      },
    },
    (app) =>
      app
        .get('/admin', () => 'admin')
        .get('/settings', () => 'settings'),
  )
  .get('/public', () => 'public') // not affected by guard
  .listen(3000)

Derive & Resolve

derive extends the context before validation (runs in the transform queue):

new Goddo()
  .derive(({ headers }) => ({
    bearer: headers.authorization?.replace('Bearer ', ''),
  }))
  .get('/token', ({ bearer }) => bearer)

resolve extends the context after validation (runs in the beforeHandle queue):

new Goddo()
  .resolve(async ({ headers }) => ({
    user: await getUser(headers.authorization),
  }))
  .get('/me', ({ user }) => user.name)

Divergence from Elysia: onCleanup (Teardown)

onCleanup is a context method that registers a teardown function, run asynchronously in the finally block after the request finishes. Ideal for cleaning up request-scoped resources.

new Goddo()
  .derive(({ onCleanup }) => {
    const tx = db.transaction()
    onCleanup(() => tx.release())
    return { tx }
  })

Macro

Create reusable route-level options that expand into lifecycle hooks:

new Goddo()
  .macro({
    auth: (enabled: boolean) => ({
      beforeHandle({ headers, error }) {
        if (enabled && !headers.authorization) throw error(401)
      },
    }),
  })
  .get('/', () => 'public')
  .get('/admin', () => 'secret', { auth: true }) // macro applied
  .listen(3000)

WebSockets (.ws)

Elysia-compatible WebSocket support, with built-in schema validation and pub/sub rooms, powered natively by Deno.upgradeWebSocket:

import { Goddo, t } from '@goddo/core'

new Goddo()
  .ws('/chat', {
    body: t.Object({ text: t.String() }),
    open(ws) {
      ws.subscribe('general')
      ws.publish('general', { text: 'A user joined' })
    },
    message(ws, msg) {
      ws.publish('general', msg)
    },
    close(ws) {
      ws.unsubscribe('general')
    },
  })
  .listen(3000)

Lifecycle

onRequest → onParse → onTransform → derive → validation → resolve → onBeforeHandle → handler → onAfterHandle → response validation → mapResponse → onAfterResponse (and onError for exceptions), mirroring Elysia's lifecycle.

Performance (AOT Compilation)

Goddo compiles all routes into a single optimized handler at listen() time (or when compile() is called manually):

  • Pre-merged hooks: global and route-level hooks merged once, not per-request
  • Pre-computed flags: validation uses booleans instead of truthiness checks
  • Static route map: routes without dynamic segments use a Map for O(1) lookup
  • Sucrose detection: sync handlers avoid await overhead
  • V8-optimized context: GoddoContext with hidden classes and lazy evaluation
  • Fast URL parsing: manual extraction using string indices
  • Method-aware execution: skips body parsing for GET/HEAD
const app = new Goddo()
  .get('/', () => 'Hello')
  .get('/user/:id', ({ params }) => params.id)

app.compile() // optional — listen() calls this automatically
app.listen(3000)