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: truein your route options automatically triggers the auth macro that was injected via.use(createAuthMacro(auth))increateMainRoutes(seebackend/src/api/routes.tsline 23). This validates JWT tokens before your handler executes. - CORS: The global CORS middleware configured in
createAppapplies to every mounted route, so you do not need to manually set headers (seebackend/src/index.tslines 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 inbackend/src/test-utils/to spin up an in-memory app and issue HTTP requests, following the pattern inbackend/src/api/routes.test.ts. - Swagger: If
settings.swaggerEnabledistrue, the Swagger plugin loaded increateApp(lines 42-55) introspects all Elysia routes automatically. Your new endpoint appears in the generated documentation at/v1/swaggerwithout 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 acreate*Routesfunction returning anElysiainstance. - Validate inputs using Elysia's
tschema helpers passed as the third argument to HTTP methods. - Mount the route group in
backend/src/index.tsinsidecreateAppusing.use(createYourRoutes(auth)). - Authenticate by setting
auth: truein route options; the auth macro injected increateMainRouteshandles JWT validation. - Test by creating a
*.test.tsfile next to your route using the existingtest-utilshelpers. - Document automatically via the Swagger plugin when
settings.swaggerEnabledis 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →