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

> Learn how to add new API endpoints to the Thunderbolt backend. This guide details creating Elysia route-group functions and mounting them in the createApp function.

- Repository: [Thunderbird/thunderbolt](https://github.com/thunderbird/thunderbolt)
- Tags: how-to-guide
- Published: 2026-04-19

---

**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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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:

```typescript
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()`.

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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.

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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.

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/api/hello.ts)

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/index.ts)**

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/api/hello.test.ts))**

```typescript
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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/api/account.ts) | Example of a separate route group with its own import/export pattern. |
| [`backend/src/index.ts`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/index.ts) | Server bootstrap; registers all route groups via `.use()` inside `createApp` (lines 24-88). |
| [`backend/src/auth/elysia-plugin.ts`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/auth/elysia-plugin.ts) | Provides the `Auth` type and the macro that injects authentication into routes. |
| [`backend/src/config/settings.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/config/settings.ts), the Swagger plugin loaded in [`backend/src/index.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/api/feature.test.ts) for [`backend/src/api/feature.ts`](https://github.com/thunderbird/thunderbolt/blob/main/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`](https://github.com/thunderbird/thunderbolt/blob/main/backend/src/api/routes.test.ts).