How to Add New API Endpoints to the Thunderbolt Backend: A Complete Guide

You add new API endpoints to Thunderbolt by creating an Elysia route-group function in backend/src/api/ that returns a configured Elysia instance, then mounting it via .use() in the createApp function of backend/src/index.ts.

Thunderbolt is Thunderbird's modern backend service built on the Elysia framework. If you need to add new API endpoints to the Thunderbolt backend, you will follow a composable, file-based routing pattern that leverages TypeScript-first validation and automatic OpenAPI documentation.

Step 1: Create the Route Handler in backend/src/api/

All HTTP routes in Thunderbolt live as small, composable Elysia instances. Create a new file in backend/src/api/ (for example, backend/src/api/hello.ts) to hold your endpoint logic.

The handler receives a context object (ctx) containing query, body, set (response metadata), and other Elysia-specific properties. A minimal handler looks like this:

import { Elysia } from 'elysia'

export const createHelloRoutes = () => {
  return new Elysia()
    .get('/hello', (ctx) => {
      const name = ctx.query.name ?? 'world'
      return { message: `Hello, ${name}!` }
    })
}

Step 2: Add Validation Using Elysia Schemas

Thunderbolt uses Elysia's built-in schema helpers to type-check query and body parameters at runtime. Import t from elysia and pass a validation object as the third argument to .get(), .post(), .put(), or .delete().

import { Elysia, t } from 'elysia'

export const createHelloRoutes = () => {
  return new Elysia()
    .get(
      '/hello',
      (ctx) => {
        const name = ctx.query.name ?? 'world'
        return { message: `Hello, ${name}!` }
      },
      {
        query: t.Object({
          name: t.Optional(t.String()),
        }),
        response: t.Object({
          message: t.String(),
        }),
      }
    )
}

Step 3: Export a Route-Group Creator Function

Following the pattern established in backend/src/api/routes.ts, wrap your routes in a creator function that receives shared dependencies (such as the Auth plugin or a custom fetch wrapper) and returns the configured Elysia instance. This dependency injection pattern keeps routes testable and decoupled.

import { Elysia, t } from 'elysia'
import type { Auth } from '@/auth/elysia-plugin'

export const createHelloRoutes = (auth: Auth) => {
  return new Elysia()
    .get(
      '/hello',
      (ctx) => {
        const name = ctx.query.name ?? 'world'
        return { message: `Hello, ${name}!` }
      },
      {
        auth: true, // <-- uses the auth macro injected globally
        query: t.Object({
          name: t.Optional(t.String()),
        }),
      }
    )
}

Step 4: Mount the Route-Group in createApp

Open backend/src/index.ts and locate the createApp function. This is where all route groups are assembled into the final application. Import your new route creator and add it to the chain of .use() calls.

import { createHelloRoutes } from '@/api/hello'   // ← new import

// Inside the createApp function:
export const createApp = async () => {
  const app = new Elysia()
    .use(createMainRoutes(auth))
    .use(createAccountRoutes(auth))
    .use(createHelloRoutes(auth))   // ← mount the new endpoint
    // ... other middleware
}

The createApp function applies a /v1 prefix to all routes (as seen in backend/src/index.ts lines 24-40), so your new endpoint becomes available at GET /v1/hello.

Step 5: Configure Authentication and Middleware

Thunderbolt centralizes cross-cutting concerns like authentication and CORS in the main application builder.

  • Authentication: Setting auth: true in your route options automatically triggers the auth macro that was injected via .use(createAuthMacro(auth)) in createMainRoutes (see backend/src/api/routes.ts line 23). This validates JWT tokens before your handler executes.
  • CORS: The global CORS middleware configured in createApp applies to every mounted route, so you do not need to manually set headers (see backend/src/index.ts lines 71-81).

Step 6: Write Tests and Verify Swagger Documentation

Testing and documentation are generated automatically when you follow the established patterns.

  • Testing: Create a test file adjacent to your route (e.g., backend/src/api/hello.test.ts). Use the existing test helpers in backend/src/test-utils/ to spin up an in-memory app and issue HTTP requests, following the pattern in backend/src/api/routes.test.ts.
  • Swagger: If settings.swaggerEnabled is true, the Swagger plugin loaded in createApp (lines 42-55) introspects all Elysia routes automatically. Your new endpoint appears in the generated documentation at /v1/swagger without requiring additional configuration.

Complete Example: Adding a /hello Endpoint

File: backend/src/api/hello.ts

import { Elysia, t } from 'elysia'
import type { Auth } from '@/auth/elysia-plugin'

/**
 * Exported creator that registers the `/hello` endpoint.
 * The route requires authentication and validates an optional `name` query param.
 */
export const createHelloRoutes = (auth: Auth) => {
  return new Elysia()
    .get(
      '/hello',
      (ctx) => {
        const name = ctx.query.name ?? 'world'
        return { message: `Hello, ${name}!` }
      },
      {
        auth: true,
        query: t.Object({
          name: t.Optional(t.String()),
        }),
      }
    )
}

Mounting in backend/src/index.ts

import { createHelloRoutes } from '@/api/hello'   // ← add this import

// Inside createApp's return chain:
.use(createHelloRoutes(auth))                    // ← mount the new routes

Test stub (backend/src/api/hello.test.ts)

import { describe, expect, test } from 'bun:test'
import { createApp } from '@/index'
import request from 'supertest'

describe('hello endpoint', () => {
  test('returns greeting with default name', async () => {
    const app = await createApp()
    const res = await request(app.handle).get('/v1/hello')
    expect(res.statusCode).toBe(200)
    expect(res.body).toEqual({ message: 'Hello, world!' })
  })
})

Key Files and References

Path Purpose
backend/src/api/routes.ts Core “main” routes (health, units, locations). Shows the canonical pattern for route creation in createMainRoutes (lines 20-34).
backend/src/api/account.ts Example of a separate route group with its own import/export pattern.
backend/src/index.ts Server bootstrap; registers all route groups via .use() inside createApp (lines 24-88).
backend/src/auth/elysia-plugin.ts Provides the Auth type and the macro that injects authentication into routes.
backend/src/config/settings.ts Holds feature flags (e.g., swaggerEnabled, rateLimitEnabled) that affect route behavior.
backend/src/middleware/*.ts Middleware (CORS, logging, error handling) automatically applied to every route.
backend/src/api/*.test.ts Test suites for existing endpoints – use as a template for new endpoint tests.

Summary

  • Create a new file in backend/src/api/ that exports a create*Routes function returning an Elysia instance.
  • Validate inputs using Elysia's t schema helpers passed as the third argument to HTTP methods.
  • Mount the route group in backend/src/index.ts inside createApp using .use(createYourRoutes(auth)).
  • Authenticate by setting auth: true in route options; the auth macro injected in createMainRoutes handles JWT validation.
  • Test by creating a *.test.ts file next to your route using the existing test-utils helpers.
  • Document automatically via the Swagger plugin when settings.swaggerEnabled is true.

Frequently Asked Questions

How do I add authentication to a new endpoint in Thunderbolt?

Set auth: true in the route options object (the third argument to .get(), .post(), etc.). This triggers the authentication macro that was injected into the Elysia instance via .use(createAuthMacro(auth)) in backend/src/api/routes.ts. The macro validates JWT tokens before your handler executes.

Where do I place validation schemas for request parameters?

Define validation schemas inline using Elysia's t object (imported from elysia) as the third argument to your route definition. For example, pass query: t.Object({ name: t.Optional(t.String()) }) to validate query parameters. Elysia performs the validation automatically before calling your handler.

Does Thunderbolt automatically generate API documentation for new endpoints?

Yes. If settings.swaggerEnabled is true in backend/src/config/settings.ts, the Swagger plugin loaded in backend/src/index.ts automatically introspects all registered Elysia routes. Your new endpoint appears at /v1/swagger without requiring manual documentation updates.

How should I structure unit tests for a new route?

Create a test file adjacent to your route implementation (e.g., backend/src/api/feature.test.ts for backend/src/api/feature.ts). Import createApp from @/index and use the test utilities in backend/src/test-utils/ to spin up an in-memory server and issue HTTP requests, following the pattern in backend/src/api/routes.test.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →