# How to Add a New Feature to the Kaneo Monorepo: A Complete Development Guide

> Learn how to add a new feature to the Kaneo monorepo. This guide covers database schema updates, controller creation, Hono route exposure, and TanStack Query hook implementation for seamless integration.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: how-to-guide
- Published: 2026-08-09

---

**Adding a feature to Kaneo requires updating the database schema in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts), creating a controller in `apps/api/src/<feature>/controllers/`, exposing a Hono route in `apps/api/src/<feature>/index.ts`, and consuming it via a TanStack Query hook in `apps/web/src/hooks/queries/<feature>/`.**

Kaneo is organized as a **pnpm monorepo** with separate `apps/api` (backend) and `apps/web` (frontend) packages. When you add a new feature to the kaneo monorepo, you work across the entire stack—from database schema definitions to React UI components—following strict architectural conventions documented in [`CLAUDE.md`](https://github.com/usekaneo/kaneo/blob/main/CLAUDE.md) and [`CONTRIBUTING.md`](https://github.com/usekaneo/kaneo/blob/main/CONTRIBUTING.md).

## Planning the Feature

Before writing code, decide what data your feature needs and whether it requires database persistence. Determine which UI components will consume the new functionality and whether you need queries (read) or mutations (write). Kaneo uses **Drizzle ORM** for database operations, **Hono** for API routes, **Valibot** for validation, and **TanStack Query** for server state management on the frontend.

## Backend Implementation

### Database Schema Updates

If your feature requires data persistence, start by defining the table in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts). Kaneo uses Drizzle ORM with PostgreSQL.

```ts
// apps/api/src/database/schema.ts
export const tagTable = pgTable('tag', {
  id: text('id')
    .$defaultFn(() => createId())
    .primaryKey(),
  name: text('name').notNull(),
  createdAt: timestamp('created_at', { mode: 'date' })
    .defaultNow()
    .notNull(),
});

```

After modifying the schema, generate a migration using the package filter:

```bash
pnpm --filter @kaneo/api db:generate

```

### Creating Controllers

Business logic lives in feature-specific controller files under `apps/api/src/<feature>/controllers/`. These functions handle database interactions and return clean data structures.

```ts
// apps/api/src/tag/controllers/create-tag.ts
import { db } from '@/db';
import { tagTable } from '@/database/schema';

export async function createTag(name: string) {
  const id = await db
    .insert(tagTable)
    .values({ name })
    .returning({ id: tagTable.id });
  return { id: id[0].id, name };
}

```

### Defining API Routes with Hono

Routes are defined per-feature in `apps/api/src/<feature>/index.ts` using **Hono** and `describeRoute` from `hono-openapi` for automatic OpenAPI documentation.

```ts
// apps/api/src/tag/index.ts
import { Hono } from 'hono';
import { describeRoute, validator } from 'hono-openapi';
import * as v from 'valibot';
import { createTag } from './controllers/create-tag';

export const tag = new Hono()
  .post(
    '/',
    describeRoute({
      operationId: 'createTag',
      tags: ['Tag'],
      description: 'Create a new tag',
    }),
    validator('json', v.object({ name: v.string() })),
    async (c) => {
      const { name } = c.req.valid('json');
      const tag = await createTag(name);
      return c.json(tag, 201);
    }
  );

```

### Input Validation with Valibot

Kaneo uses **Valibot** schemas to validate incoming request data. The `validator` middleware from `hono-openapi` applies these schemas before your route handler executes, ensuring type safety at runtime.

## Frontend Implementation

### API Fetchers

Frontend code communicates with the backend via small fetcher functions located in `apps/web/src/fetchers/<feature>/`. These thin wrappers handle the actual HTTP requests and error handling.

```ts
// apps/web/src/fetchers/tag/create-tag.ts
import { getApiUrl } from '@/fetchers/get-api-url';

export async function createTag(name: string) {
  const res = await fetch(`${getApiUrl()}/tag`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name }),
  });
  if (!res.ok) throw new Error('Failed to create tag');
  return res.json();
}

```

### TanStack Query Hooks

Wrap fetchers in custom hooks under `apps/web/src/hooks/queries/<feature>/` (for reads) or `apps/web/src/hooks/mutations/<feature>/` (for writes). This provides caching, background updates, and error handling.

```ts
// apps/web/src/hooks/mutations/tag/use-create-tag.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTag } from '@/fetchers/tag/create-tag';

export function useCreateTag() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: createTag,
    onSuccess: () => {
      // Invalidate tag list queries so UI updates
      qc.invalidateQueries({ queryKey: ['tags'] });
    },
  });
}

```

### UI Components

Integrate hooks into your React components. Kaneo uses a file-based router in `apps/web/src/routes/` for page components, with shared UI components in `apps/web/src/components/`.

```tsx
// apps/web/src/components/tag/TagForm.tsx
import { useState } from 'react';
import { useCreateTag } from '@/hooks/mutations/tag/use-create-tag';
import { toast } from 'sonner';

export function TagForm() {
  const [name, setName] = useState('');
  const { mutate, isLoading } = useCreateTag();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    mutate(name, {
      onSuccess: () => {
        toast.success('Tag created');
        setName('');
      },
      onError: () => toast.error('Failed to create tag'),
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Tag name"
        disabled={isLoading}
      />
      <button type="submit" disabled={isLoading}>Add Tag</button>
    </form>
  );
}

```

## Testing and Quality Assurance

Unit tests live alongside the code in `*.test.ts` files within both the API and web packages. Before committing, ensure your code passes the project's quality gates:

```bash
pnpm lint
pnpm typecheck

```

All code must follow the project's **Biome** formatting rules (double quotes, semicolons) and import order conventions (external → internal → relative) as specified in [`CLAUDE.md`](https://github.com/usekaneo/kaneo/blob/main/CLAUDE.md). Commit using **Conventional Commits** (e.g., `feat: add tag API`) according to the [`CONTRIBUTING.md`](https://github.com/usekaneo/kaneo/blob/main/CONTRIBUTING.md) guidelines.

## Summary

- **Kaneo** is a pnpm monorepo with clear separation between `apps/api` and `apps/web`.
- Database changes require updates to [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) followed by migration generation.
- Backend features use **Hono** routes in `apps/api/src/<feature>/index.ts` with **Valibot** validation.
- Business logic belongs in `apps/api/src/<feature>/controllers/` for maintainability.
- Frontend data fetching uses typed fetchers in `apps/web/src/fetchers/<feature>/`.
- Server state is managed through **TanStack Query** hooks in `apps/web/src/hooks/`.
- All commits must follow Conventional Commits and pass `pnpm lint` and `pnpm typecheck`.

## Frequently Asked Questions

### Where do I define new database tables in Kaneo?

New tables are defined in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) using Drizzle ORM syntax. After adding your table definition, run `pnpm --filter @kaneo/api db:generate` to create the migration file. This follows the infrastructure-as-code pattern used throughout the kaneo monorepo.

### How does the frontend communicate with the backend API?

The frontend uses typed fetcher functions stored in `apps/web/src/fetchers/<feature>/` that wrap standard `fetch` calls. These fetchers are then consumed by TanStack Query hooks in `apps/web/src/hooks/` to provide React components with cached, reactive data access. This architecture ensures type safety from the API endpoint to the UI component.

### What validation library does Kaneo use for API inputs?

Kaneo uses **Valibot** for runtime validation, applied through the `validator` middleware from `hono-openapi` in route definitions. You define schemas (e.g., `v.object({ name: v.string() })`) that automatically validate incoming JSON before your controller logic executes, preventing invalid data from reaching the database.

### How do I run database migrations after modifying the schema?

Use the pnpm filter command to run the database generation script specifically in the API package: `pnpm --filter @kaneo/api db:generate`. This creates the migration files based on your schema changes. Apply these migrations to your development database before testing the new feature endpoints.