# Best Practices for Developing with Kaneo: A Complete Guide to the Monorepo Architecture

> Master Kaneo development with these best practices. Learn to leverage its monorepo, TurboRepo, Hono routes, Valibot validation, and controller pattern for efficient, maintainable code.

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

---

**The best practices for developing with Kaneo center on maintaining its pnpm monorepo structure, using TurboRepo for build orchestration, implementing thin Hono routes with Valibot validation, and following the controller pattern to separate business logic from HTTP handling.**

Kaneo is a self-hosted project management platform built as a modern **pnpm monorepo** using **TurboRepo**. Developing effectively requires understanding its architectural conventions—from the controller-based API design in `apps/api` to the TanStack Query patterns in `apps/web`. This guide covers the essential practices derived from the Kaneo source code to keep your contributions clean, performant, and consistent with the codebase.

## 1. Maintain the Monorepo Structure with pnpm and TurboRepo

Kaneo organizes code into three main applications under `apps/` (API, web, docs) plus shared libraries in `packages/`. Keep this structure intact when adding features.

The [`pnpm-workspace.yaml`](https://github.com/usekaneo/kaneo/blob/main/pnpm-workspace.yaml) at the repository root defines workspace boundaries. Always run commands from the root so TurboRepo can orchestrate builds across the dependency graph:

```bash
pnpm dev       # Launches API and web in watch mode

pnpm lint      # Runs Biome linting across all packages

pnpm typecheck # Type-checks every package

pnpm build     # Produces production bundles

```

Avoid running npm or yarn commands—pnpm is the required package manager for this workspace.

## 2. Architect the API Using Controllers and Validation

The API in `apps/api` uses **Hono** with a strict separation between routes and business logic.

### Thin Routes and Controllers

Business logic belongs in controller files under `apps/api/src/<feature>/controllers/`. Routes should remain thin wrappers that apply validation, authentication, and OpenAPI metadata.

For example, [`apps/api/src/workspace/controllers/get-workspace-members.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/workspace/controllers/get-workspace-members.ts) contains the database query logic, while the route handler only validates inputs and calls the controller:

```ts
// apps/api/src/example/index.ts
import { Hono } from "hono";
import { describeRoute, validator } from "hono-openapi";
import * as v from "valibot";
import getExample from "./controllers/get-example";

const example = new Hono<{ Variables: { userId: string } }>()
  .get(
    "/:id",
    describeRoute({
      operationId: "getExample",
      tags: ["Example"],
      description: "Fetch an example resource by ID",
    }),
    validator("param", v.object({ id: v.string() })),
    async (c) => {
      const { id } = c.req.valid("param");
      const result = await getExample(id);
      return c.json(result);
    },
  );

export default example;

```

### Input Validation with Valibot

Use **Valibot** (or Zod where required) for schema validation. The `validator` helper from `hono-openapi` attaches schemas to route parameters, body, or query parameters. This ensures type safety and automatic OpenAPI spec generation.

### OpenAPI Documentation

Every route must use `describeRoute` to generate the OpenAPI specification. The spec generator lives in [`apps/api/src/utils/openapi-spec.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/utils/openapi-spec.ts). Documenting routes here enables the auto-generated API documentation.

### Authentication Handling

**Better Auth** provides JWT-based authentication. Access the current user in request handlers via `c.get("user")` as implemented in [`apps/api/src/auth.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth.ts). Always protect routes that require authentication using the appropriate middleware.

### Database Conventions

**Drizzle ORM** models live in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts). Follow these conventions:

- Use **CUID2** for all ID fields
- Include `createdAt` and `updatedAt` timestamps on all tables
- Keep schema definitions in the central schema file

## 3. Build the Front-End with TanStack Query and Zustand

The web application in `apps/web` uses React with specific patterns for data fetching and state management.

### Data Fetching Patterns

Encapsulate API calls in "fetchers" under `apps/web/src/fetchers/`, then consume them via custom hooks in `apps/web/src/hooks/queries/`.

For example, [`apps/web/src/fetchers/workspace/get-workspaces.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/fetchers/workspace/get-workspaces.ts) handles the HTTP request:

```ts
// apps/web/src/fetchers/example/get-example.ts
import { apiUrl } from "@/lib/api-url";

export async function getExample(id: string) {
  const res = await fetch(`${apiUrl}/example/${id}`, {
    credentials: "include",
  });
  if (!res.ok) throw new Error("Failed to fetch example");
  return res.json();
}

```

Then create a TanStack Query hook:

```ts
// apps/web/src/hooks/queries/example/useExample.ts
import { useQuery } from "@tanstack/react-query";
import { getExample } from "@/fetchers/example/get-example";

export function useExample(id: string) {
  return useQuery(["example", id], () => getExample(id));
}

```

### State Management

Use **Zustand** for global UI state. Follow the existing store patterns in `apps/web/src/lib/` to ensure consistency across features.

### UI Components and Styling

Build UI components using **Radix UI primitives** and **Tailwind v4**. Place components in `apps/web/src/components/` using PascalCase naming. This ensures accessibility and consistent styling across the application.

### Internationalization Requirements

All user-visible strings must use `react-i18next`. Add keys to [`i18n/en-US.json`](https://github.com/usekaneo/kaneo/blob/main/i18n/en-US.json) first, then reference them via `t("namespace:key")` syntax.

Example implementation:

```json
// i18n/en-US.json
{
  "common": {
    "actions": {
      "close": "Close"
    }
  },
  "tasks": {
    "greeting": "Hello, {{name}}!"
  }
}

```

```tsx
// apps/web/src/components/TaskGreeting.tsx
import { useTranslation } from "react-i18next";

export function TaskGreeting({ name }: { name: string }) {
  const { t } = useTranslation();
  return <p>{t("tasks:greeting", { name })}</p>;
}

```

See [`CONTRIBUTING.md`](https://github.com/usekaneo/kaneo/blob/main/CONTRIBUTING.md) for detailed i18n workflow instructions.

## 4. Configure Environment Variables Correctly

Kaneo uses a single `.env` file in the repository root to supply variables to both the API and web services.

Required variables include:

- `KANEO_CLIENT_URL`
- `KANEO_API_URL`
- `AUTH_SECRET`
- PostgreSQL credentials

See [`ENVIRONMENT_SETUP.md`](https://github.com/usekaneo/kaneo/blob/main/ENVIRONMENT_SETUP.md) for the complete list.

### CORS Configuration

During local development, you can leave `CORS_ORIGINS` empty. For production, set it to the exact origin of the web app. The API reads this variable in [`apps/api/src/config/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/config/index.ts).

### Redis for Horizontal Scaling

For multi-instance deployments, configure Redis via `REDIS_URL`, Sentinel, or Cluster mode. The WebSocket broadcast adapters in `apps/api/src/ws/` ([`in-memory-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/in-memory-broadcast-adapter.ts) and [`redis-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/redis-broadcast-adapter.ts)) handle this automatically based on environment variables.

## 5. Enforce Code Quality with Biome and Conventional Commits

**Biome** is the sole formatter and linter. Run `pnpm lint` before committing; the pre-commit hook enforces this automatically.

Style requirements:

- **Double quotes** everywhere
- **Semicolons** required
- **Import ordering**: external → internal (`@/` alias) → relative

Commit messages must follow **Conventional Commits** (`feat:`, `fix:`, `docs:`, etc.). The commit-lint configuration lives in `.husky/commit-msg`.

## 6. Implement Comprehensive Testing

Maintain code quality through unit and integration tests.

### Unit Testing

**Vitest** unit tests live alongside source files as `*.test.ts`. The API has its own [`vitest.config.ts`](https://github.com/usekaneo/kaneo/blob/main/vitest.config.ts) under `apps/api/`; the web app has one under `apps/web/`.

### Integration Testing

Integration tests use a real PostgreSQL instance and reside in `tests/api-integration/`. For example, [`tests/api-integration/project.test.ts`](https://github.com/usekaneo/kaneo/blob/main/tests/api-integration/project.test.ts) validates end-to-end project workflows.

Run the full suite before pushing:

```bash
pnpm test             # Unit tests

pnpm test:integration # Integration tests

```

## Summary

- **Maintain monorepo integrity** by using pnpm commands from the root and respecting the `apps/` and `packages/` structure
- **Separate API concerns** using controllers for business logic, Valibot for validation, and `describeRoute` for OpenAPI documentation
- **Follow front-end patterns** by placing fetchers in `apps/web/src/fetchers/`, TanStack Query hooks in `apps/web/src/hooks/queries/`, and using Zustand for global state
- **Configure environment variables** in a root `.env` file, including optional Redis settings for scaling
- **Enforce code quality** with Biome formatting, import ordering, and Conventional Commits
- **Write tests** using Vitest for unit tests and PostgreSQL-backed integration tests in `tests/api-integration/`

## Frequently Asked Questions

### How do I add a new API endpoint in Kaneo?

Create a controller file in `apps/api/src/<feature>/controllers/` containing your business logic. Then define a thin route in `apps/api/src/<feature>/index.ts` using Hono, applying Valibot validation via `validator()` and OpenAPI metadata via `describeRoute()`. This keeps HTTP handling separate from database queries.

### What is the correct way to fetch data from the front-end?

Create a fetcher function in `apps/web/src/fetchers/` that calls `fetch()` with `credentials: "include"`. Then create a custom hook in `apps/web/src/hooks/queries/` using `useQuery` from TanStack Query. Import the fetcher into your hook and component to consume the data.

### How should I handle authentication in the API?

Use the Better Auth middleware to protect routes. Access the authenticated user in your handlers via `c.get("user")` as defined in [`apps/api/src/auth.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth.ts). The middleware validates JWT tokens automatically, making user information available to downstream controllers.

### What testing strategy does Kaneo require?

Write unit tests as `*.test.ts` files alongside your source code using Vitest. For end-to-end validation, add integration tests in `tests/api-integration/` that test against a real PostgreSQL instance. Run `pnpm test` for unit tests and `pnpm test:integration` for integration tests before submitting changes.