Skip to main content
This guide walks you through integrating tsoa with a Koa.js application, from initial setup to running your API server.

Installation

1

Install Dependencies

Install tsoa along with Koa and its dependencies:
npm install tsoa koa @koa/router koa-bodyparser
npm install --save-dev @types/koa @types/koa__router @types/node typescript
2

Configure tsoa

Create a tsoa.json configuration file in your project root:
tsoa.json
{
  "entryFile": "src/app.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "spec": {
    "outputDirectory": "build",
    "specVersion": 3
  },
  "routes": {
    "routesDir": "src",
    "middleware": "koa"
  }
}
The middleware field must be set to "koa" for Koa.js projects.
3

Create a Controller

Create your first controller with tsoa decorators:
src/controllers/userController.ts
import { Controller, Get, Post, Route, Body, Path, SuccessResponse } from 'tsoa';

interface User {
  id: number;
  name: string;
  email: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
}

@Route('users')
export class UserController extends Controller {
  /**
   * Retrieves a user by ID
   * @param userId The user's identifier
   */
  @Get('{userId}')
  public async getUser(@Path() userId: number): Promise<User> {
    return {
      id: userId,
      name: 'John Doe',
      email: '[email protected]'
    };
  }

  /**
   * Creates a new user
   */
  @Post()
  @SuccessResponse('201', 'Created')
  public async createUser(@Body() requestBody: CreateUserRequest): Promise<User> {
    this.setStatus(201);
    return {
      id: Math.floor(Math.random() * 10000),
      ...requestBody
    };
  }
}
4

Set Up Koa Server

Create your Koa application and register tsoa routes:
src/app.ts
import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';
import { RegisterRoutes } from './routes';

const app = new Koa();
const router = new Router();

// Middleware
app.use(bodyParser());

// Register tsoa routes
RegisterRoutes(router);

// Error handling - must come before routes
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err: any) {
    ctx.status = err.status || 500;
    ctx.body = {
      message: err.message || 'An error occurred during the request.',
      status: ctx.status,
      fields: err.fields
    };
  }
});

// Apply routes
app.use(router.routes());
app.use(router.allowedMethods());

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});
5

Generate Routes and Spec

Add scripts to your package.json:
package.json
{
  "scripts": {
    "build": "tsoa spec-and-routes && tsc",
    "start": "node build/app.js",
    "dev": "tsoa spec-and-routes && ts-node src/app.ts"
  }
}
Generate routes and OpenAPI spec:
npm run build
This creates:
  • src/routes.ts - Generated route handlers
  • build/swagger.json - OpenAPI specification
6

Start the Server

npm start
Your API is now running at http://localhost:3000!Test it:
curl http://localhost:3000/users/1

Koa Context Access

Access the Koa context object in your controllers:
import { Controller, Get, Request } from 'tsoa';
import { Context } from 'koa';

@Route('example')
export class ExampleController extends Controller {
  @Get('with-context')
  public async withContext(
    @Request() ctx: Context
  ): Promise<any> {
    // Access request headers
    const userAgent = ctx.headers['user-agent'];
    
    // Access custom state
    const userId = ctx.state.userId;
    
    // Set response headers
    ctx.set('X-Custom-Header', 'value');
    
    return { userAgent, userId };
  }
}

Middleware Integration

Apply Koa middleware to specific routes or controllers:
import { Controller, Get, Middlewares } from 'tsoa';
import { Context, Next, Middleware } from 'koa';

const authMiddleware: Middleware = async (ctx: Context, next: Next) => {
  if (ctx.headers.authorization) {
    // Set user info in context state
    ctx.state.user = await verifyToken(ctx.headers.authorization);
    await next();
  } else {
    ctx.throw(401, 'Unauthorized');
  }
};

const loggingMiddleware: Middleware = async (ctx: Context, next: Next) => {
  console.log(`${ctx.method} ${ctx.path}`);
  await next();
};

@Route('protected')
@Middlewares(authMiddleware)
export class ProtectedController extends Controller {
  @Get('data')
  @Middlewares(loggingMiddleware)
  public async getData(): Promise<any> {
    return { data: 'sensitive information' };
  }
}

Error Handling

Koa uses a different error handling pattern than Express:
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err: any) {
    ctx.status = err.status || 500;
    ctx.body = {
      message: err.message,
      status: ctx.status
    };
  }
});

Using ctx.throw

Koa provides a convenient ctx.throw() method:
import { Controller, Get, Path, Request } from 'tsoa';
import { Context } from 'koa';

@Route('items')
export class ItemController extends Controller {
  @Get('{id}')
  public async getItem(
    @Path() id: number,
    @Request() ctx: Context
  ): Promise<Item> {
    const item = await findItem(id);
    
    if (!item) {
      ctx.throw(404, 'Item not found');
    }
    
    return item;
  }
}

State Management

Share data between middleware using ctx.state:
// Middleware to set state
const userMiddleware: Middleware = async (ctx, next) => {
  const token = ctx.headers.authorization;
  ctx.state.user = await getUserFromToken(token);
  await next();
};

// Controller accessing state
@Route('profile')
@Middlewares(userMiddleware)
export class ProfileController extends Controller {
  @Get()
  public async getProfile(@Request() ctx: Context): Promise<User> {
    return ctx.state.user;
  }
}

Base Path Configuration

Configure a base path for all routes:
tsoa.json
{
  "routes": {
    "routesDir": "src",
    "middleware": "koa",
    "basePath": "/api/v1"
  }
}
All routes will be prefixed with /api/v1.

Router Prefix

Alternatively, use Koa Router’s prefix option:
const router = new Router({
  prefix: '/api/v1'
});

RegisterRoutes(router);

Query Parameters

import { Controller, Get, Query } from 'tsoa';

@Route('search')
export class SearchController extends Controller {
  @Get()
  public async search(
    @Query() q: string,
    @Query() limit?: number,
    @Query() offset?: number
  ): Promise<SearchResult[]> {
    return performSearch(q, limit || 10, offset || 0);
  }
}

Headers

import { Controller, Get, Header, Request } from 'tsoa';
import { Context } from 'koa';

@Route('api')
export class ApiController extends Controller {
  @Get('version')
  public async getVersion(
    @Header('x-api-version') apiVersion?: string,
    @Request() ctx: Context
  ): Promise<any> {
    // Set response header
    ctx.set('X-Response-Version', '1.0');
    
    return { requestedVersion: apiVersion };
  }
}

Comparison with Express

import express from 'express';
import { RegisterRoutes } from './routes';

const app = express();
RegisterRoutes(app);

app.use((err, req, res, next) => {
  res.status(err.status || 500).json(err);
});

Next Steps

File Uploads

Learn how to handle file uploads with Koa

Authentication

Secure your API with authentication

Validation

Implement request validation

Dependency Injection

Use IoC containers with your controllers

Build docs developers (and LLMs) love