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

Adding a feature to Kaneo requires updating the database schema in 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 and 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. Kaneo uses Drizzle ORM with PostgreSQL.

// 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:

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.

// 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.

// 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.

// 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.

// 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/.

// 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:

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. Commit using Conventional Commits (e.g., feat: add tag API) according to the 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 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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →