# How API Routes Are Organized in Kaneo: A Modular Hono Architecture

> Discover how Kaneo organizes its API routes using a modular Hono architecture. Learn about feature-based modules, automatic OpenAPI docs, and a flat URL hierarchy for efficient development.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: architecture
- Published: 2026-08-29

---

**Kaneo organizes its HTTP API into feature-based modules using Hono and @hono/zod-openapi, where each domain (workspace, task, project) maintains its own router in `apps/api/src/` and registers routes via `createRoute` with automatic OpenAPI documentation, then mounts them in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) for a flat URL hierarchy.**

Kaneo is an open-source project management platform built with modern TypeScript tooling. Understanding how API routes are organized in Kaneo reveals a clean, modular architecture that leverages the **Hono** web framework combined with Zod-based OpenAPI validation. This design separates concerns by feature while maintaining type safety and automatic API documentation generation.

## Project Structure and Routing Philosophy

Kaneo’s API follows a **feature-based directory structure** where each logical domain lives in its own subdirectory under `apps/api/src/`. The architecture rests on three core principles:

- **Declarative route definitions** using the `createRoute` helper from [`apps/api/src/openapi.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/openapi.ts), which bundles HTTP methods, paths, OpenAPI metadata, request validation, and response schemas
- **Feature isolation** with each module (workspace, task, label, project, auth) exporting its own router created via `apiRouter<BaseVariables>()`
- **Flat URL hierarchy** achieved by mounting feature routers at specific prefixes in the main entry point, resulting in endpoints like `/api/workspace/{workspaceId}/members` and `/api/task/{id}`

## The createRoute Abstraction

All route definitions in Kaneo rely on `createRoute`, a thin wrapper defined in [`apps/api/src/openapi.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/openapi.ts) that standardizes endpoint configuration. This utility accepts an object specifying the HTTP method, operation ID, tags, summary, description, optional middleware, request validation, and response schemas.

```typescript
// Conceptual schema from apps/api/src/openapi.ts
createRoute({
  method: "get",
  path: "/{workspaceId}/ping",
  operationId: "pingWorkspace",
  tags: ["Workspaces"],
  summary: "Health-check for a workspace",
  middleware: [workspaceAccess.fromParam("workspaceId")],
  responses: {
    200: jsonResponse("Workspace is reachable", z.object({ ok: z.boolean() })),
    400: errorResponse("Workspace ID could not be determined"),
  },
});

```

The `createRoute` function returns a route definition that is then attached to a feature router using `.openapi(route, handler)`.

## Feature-Based Router Organization

Each feature module exports a router created with `apiRouter<BaseVariables>()` or `apiRouter<BaseVariables & { workspaceId: string }>()` when workspace validation is required. These routers chain `.openapi()` calls to register multiple endpoints.

### Workspace Router Implementation

The workspace feature demonstrates how to implement protected routes with workspace validation. Located at [`apps/api/src/workspace/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/index.ts), this router uses `workspaceAccess` middleware to resolve workspace IDs and check permissions before handlers execute.

```typescript
// apps/api/src/workspace/index.ts
import { apiRouter, createRoute, jsonResponse, errorResponse } from "../openapi";
import { workspaceAccess } from "../utils/workspace-access-middleware";

const pingRoute = createRoute({
  method: "get",
  operationId: "pingWorkspace",
  path: "/{workspaceId}/ping",
  tags: ["Workspaces"],
  summary: "Health-check for a workspace",
  description: "Returns a simple OK response for the supplied workspace.",
  middleware: [workspaceAccess.fromParam("workspaceId")] as const,
  responses: {
    200: jsonResponse("Workspace is reachable", z.object({ ok: z.boolean() })),
    400: errorResponse("Workspace ID could not be determined"),
    403: errorResponse("No access to the workspace"),
  },
});

const workspace = apiRouter<BaseVariables & { workspaceId: string }>()
  .openapi(pingRoute, async (c) => {
    // The middleware already ensures the caller can see the workspace.
    return c.json({ ok: true }, 200);
  });

export default workspace;

```

### Task Router Pattern

For larger feature sets, examine [`apps/api/src/task/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/task/index.ts). This file illustrates a complete CRUD implementation with multiple routes, validation schemas, and middleware chains. The task router uses `apiRouter<BaseVariables & { workspaceId: string }>()` to ensure all endpoints within the task domain validate workspace access consistently.

## Middleware and Cross-Cutting Concerns

Kaneo extracts reusable authentication and authorization logic into dedicated middleware utilities:

- **Authentication**: The `authenticateApiRequest` middleware in [`apps/api/src/utils/authenticate-api-request.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/authenticate-api-request.ts) validates sessions or API keys and is typically applied globally in the main entry point via `api.use("*", authenticateApiRequest)`
- **Workspace Access**: The `workspaceAccess` helper in [`apps/api/src/utils/workspace-access-middleware.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/workspace-access-middleware.ts) provides methods like `fromProject()`, `fromTask()`, and `fromParam()` to resolve workspace IDs from various request contexts
- **Permission Checks**: Additional utilities like `requireWorkspacePermission` in [`apps/api/src/utils/require-workspace-permission.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/require-workspace-permission.ts) enforce fine-grained access control on specific routes

## Mounting Routes in the Main Application

The root API server in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) assembles all feature routers and applies global middleware. It imports each feature router and mounts it on a path prefix using `api.route("<prefix>", router)`.

```typescript
// apps/api/src/index.ts (excerpt)
import workspace from "./workspace";
import task from "./task";
import project from "./project";
import label from "./label";
import auth from "./auth";

// Global middleware applied to all routes
api.use("*", authenticateApiRequest);

// Mount feature routers
api.route("/workspace", workspace);
api.route("/task", task);
api.route("/project", project);
api.route("/label", label);
api.route("/auth", auth);

```

This mounting strategy produces the flat URL hierarchy observed in the API:
- `/api/workspace/{workspaceId}/members`
- `/api/task/{id}`
- `/api/project/{projectId}`
- `/api/auth/get-session`

## Adding a New Feature Router

To extend the API with a new domain, create a subdirectory under `apps/api/src/` following the established pattern:

1. Create the directory and index file:

```typescript
// apps/api/src/example/index.ts
import { apiRouter, createRoute, jsonResponse } from "../openapi";
import { z } from "zod";

const helloRoute = createRoute({
  method: "get",
  operationId: "sayHello",
  path: "/hello",
  tags: ["Example"],
  summary: "Demo endpoint",
  description: "Returns a friendly greeting.",
  responses: { 
    200: jsonResponse("Greeting", z.object({ message: z.string() })) 
  },
});

const example = apiRouter()
  .openapi(helloRoute, (c) => c.json({ message: "Hello from Kaneo!" }, 200));

export default example;

```

2. Mount the router in the main entry point:

```typescript
// apps/api/src/index.ts
import example from "./example";

api.route("/example", example);

```

The new endpoint is immediately available at `GET /api/example/hello` with full OpenAPI documentation generated automatically.

## Summary

- **Kaneo uses Hono with @hono/zod-openapi** to build type-safe, self-documenting API routes in `apps/api/src/`
- **Feature-based organization** places each domain (workspace, task, auth) in its own subdirectory with its own router instance
- **Declarative route definitions** use `createRoute` from [`apps/api/src/openapi.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/openapi.ts) to bundle metadata, validation, and handlers
- **Flat URL structure** is achieved by mounting routers at specific prefixes in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts)
- **Middleware composition** handles cross-cutting concerns like authentication (`authenticateApiRequest`) and workspace validation (`workspaceAccess`) separately from business logic

## Frequently Asked Questions

### What framework does Kaneo use for its API routing?

Kaneo uses **Hono**, a lightweight, edge-ready web framework for TypeScript, extended with **@hono/zod-openapi** to provide automatic OpenAPI documentation and runtime validation. According to the Kaneo source code, the `apiRouter` function wraps Hono's router with OpenAPI capabilities.

### How does Kaneo handle authentication across API routes?

Authentication is implemented via the `authenticateApiRequest` middleware defined in [`apps/api/src/utils/authenticate-api-request.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/authenticate-api-request.ts). This middleware validates sessions or API keys and is applied globally to all routes in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts) using `api.use("*", authenticateApiRequest)`, ensuring every endpoint requires valid credentials unless explicitly excluded.

### Where are the OpenAPI schemas defined in Kaneo?

OpenAPI schemas are defined inline within each route using the `createRoute` helper exported from [`apps/api/src/openapi.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/openapi.ts). This function accepts Zod schemas for request parameters and bodies, along with response definitions via helpers like `jsonResponse` and `errorResponse`, generating the complete OpenAPI specification automatically.

### How do I add a workspace-scoped endpoint to an existing feature in Kaneo?

Create a new route constant using `createRoute` with `workspaceAccess` middleware (e.g., `workspaceAccess.fromParam("workspaceId")`), then attach it to the feature router using `.openapi(route, handler)`. Ensure the router uses the correct type parameter: `apiRouter<BaseVariables & { workspaceId: string }>()` to maintain type safety for the workspace context.