# How Karakeep Structures Its API Endpoints: A Dual-Layer tRPC and Hono Architecture

> Discover how Karakeep structures its API endpoints with a dual-layer tRPC and Hono architecture. Learn about type-safe internal logic and efficient external REST consumption.

- Repository: [Karakeep App/karakeep](https://github.com/karakeep-app/karakeep)
- Tags: architecture
- Published: 2026-07-07

---

**Karakeep organizes its API endpoints using a dual-layer architecture that combines a type-safe tRPC router tree for internal business logic with lightweight Hono HTTP routes for external REST consumption.**

The open-source bookmarking application **karakeep-app/karakeep** implements a hybrid API strategy that separates type-safe internal procedures from public HTTP interfaces. This architecture leverages tRPC for end-to-end type safety across the stack while using Hono to provide RESTful endpoints for external integrations. Understanding how these layers interact is essential for contributing to the codebase or extending the platform's functionality.

## The Dual-Layer Architecture Overview

Karakeep's backend exposes **API endpoints** through two complementary patterns working in tandem:

- **tRPC Router Tree** – A type-safe, RPC-style internal API that groups related procedures into logical modules. This handles all server-side business logic and provides the primary interface for the web client and other internal services.
- **Hono HTTP Routes** – A thin REST-style façade that forwards requests to the tRPC router or dedicated services. These public-facing endpoints handle external HTTP traffic at paths like `/api/bookmarks` and `/api/feeds`.

The tRPC layer lives primarily in `packages/trpc/`, while the Hono REST layer resides in `packages/api/routes/`. A dedicated tRPC-over-HTTP endpoint bridges these worlds at `/api/trpc`.

## The tRPC Router Tree (Internal API Core)

The tRPC implementation forms the backbone of Karakeep's **API endpoints**, providing type-safe procedures that can be called across the entire application.

### Base Helpers and Middleware

All tRPC routers build upon shared primitives exported from **[`packages/trpc/index.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/index.ts)**. This file re-exports `router` and `createScopedAuthedProcedure` functions that wrap the base `t.router` and `t.procedure` with common middleware for rate-limiting, logging, and authentication.

```typescript
// Base helpers used across all routers
export const { router, createScopedAuthedProcedure } = t;

```

These scoped procedures ensure that every API call automatically inherits proper authentication and context handling without repetitive boilerplate.

### Domain-Specific Sub-Routers

Each domain within Karakeep—bookmarks, users, tags, feeds—defines its own router file under `packages/trpc/routers/`. For example, the bookmarks router in **[`packages/trpc/routers/bookmarks.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/bookmarks.ts)** defines procedures using the scoped procedure pattern:

```typescript
export const bookmarksAppRouter = router({
  createBookmark: bookmarksProcedure
    .use(createRateLimitMiddleware({ /* ... */ }))
    .input(zNewBookmarkRequestSchema)
    .output(zBookmarkSchema.merge(z.object({ alreadyExists: z.boolean().optional() })))
    .mutation(async ({ ctx, input }) => { 
      // Implementation logic
    }),
  // Additional procedures...
});

```

Each procedure explicitly defines its input and output schemas using Zod, ensuring type safety from database to client.

### Router Composition in _app.ts

Individual sub-routers compose into a single application router in **[`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts)**. This central file imports every domain router and registers them as top-level branches:

```typescript
export const appRouter = router({
  bookmarks: bookmarksAppRouter,
  users: usersAppRouter,
  tags: tagsAppRouter,
  // Additional routers...
});

export type AppRouter = typeof appRouter;

```

The exported `AppRouter` type provides the contract that tRPC clients use to achieve end-to-end type safety.

## Hono HTTP Routes (External REST Facade)

While tRPC handles internal communication, **Hono** provides the HTTP interface that external clients consume.

### REST Endpoints Implementation

Each resource group exposes REST endpoints via Hono applications in `packages/api/routes/`. For instance, **[`packages/api/routes/bookmarks.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/routes/bookmarks.ts)** creates a Hono app that validates requests and delegates to the tRPC SDK:

```typescript
app.get("/", zValidator("query", /* schema */), async (c) => {
  const searchParams = c.req.valid("query");
  const bookmarks = await c.var.api.bookmarks.getBookmarks(searchParams);
  return c.json(adaptPagination(bookmarks), 200);
});

```

These routes utilize middleware like `authMiddleware` and `zValidator` for request validation before calling the internal tRPC methods via `c.var.api`.

### The tRPC Bridge Endpoint

The file **[`packages/api/routes/trpc.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/routes/trpc.ts)** exposes the complete tRPC router over HTTP at `/api/trpc`, allowing external clients to access the type-safe API directly:

```typescript
const trpc = new Hono<{ Variables: { ctx: Context } }>()
  .use(
    "/*",
    trpcServer({
      endpoint: "/api/trpc",
      router: appRouter,
      createContext: (_, c) => c.var.ctx,
      onError: ({ path, error }) => { 
        // Error handling logic
      },
    })
  );

export default trpc;

```

This endpoint enables the web client and other consumers to call any procedure defined in the router tree via standard HTTP POST requests.

## Practical Implementation Examples

### Calling tRPC from the Web Client

The web client consumes these **API endpoints** using the generated `AppRouter` type:

```typescript
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@karakeep/trpc/routers/_app';

export const trpc = createTRPCReact<AppRouter>();

// Usage example
const createBookmark = trpc.bookmarks.createBookmark.useMutation();
await createBookmark.mutateAsync({ url: "https://example.com", title: "My link" });

```

### Adding a New REST Endpoint

To expose a new REST endpoint, create a Hono route file in `packages/api/routes/`:

```typescript
// packages/api/routes/example.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";

const app = new Hono()
  .use(authMiddleware)
  .get(
    "/ping",
    zValidator("query", z.object({})),
    async (c) => c.json({ ok: true }, 200)
  );

export default app;

```

Register this module in the main API server (typically [`packages/api/server.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/server.ts)) to expose `/api/example/ping`.

### Extending the tRPC Router

Adding new internal procedures requires defining a sub-router and registering it in [`_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/_app.ts):

```typescript
// packages/trpc/routers/example.ts
import { router, createScopedAuthedProcedure } from "../index";
import { z } from "zod";

const exampleProcedure = createScopedAuthedProcedure("example");

export const exampleAppRouter = router({
  hello: exampleProcedure
    .input(z.object({ name: z.string() }))
    .output(z.object({ message: z.string() }))
    .query(({ input }) => ({
      message: `Hello, ${input.name}!`,
    })),
});

```

After adding `example: exampleAppRouter` to the main `appRouter` in [`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts), the procedure becomes callable at `trpc.example.hello`.

## Key Files and Their Roles

| Role | File Path |
|------|-----------|
| Top-level tRPC router | [`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts) |
| Example sub-router (bookmarks) | [`packages/trpc/routers/bookmarks.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/bookmarks.ts) |
| Common tRPC helpers | [`packages/trpc/index.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/index.ts) |
| Hono tRPC endpoint | [`packages/api/routes/trpc.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/routes/trpc.ts) |
| REST routes for bookmarks | [`packages/api/routes/bookmarks.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/routes/bookmarks.ts) |
| Authentication middleware | [`packages/api/middlewares/auth.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/middlewares/auth.ts) |

## Summary

- **Karakeep structures API endpoints** using a dual-layer approach: tRPC for internal type-safe business logic and Hono for external HTTP interfaces.
- The **tRPC router tree** in [`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts) composes domain-specific sub-routers (bookmarks, users, tags) that expose typed procedures via `router({ ... })`.
- **Hono routes** in `packages/api/routes/` provide RESTful façade endpoints that validate requests and delegate to the internal tRPC SDK.
- The **tRPC-over-HTTP endpoint** at `/api/trpc` exposes the entire router tree for clients that can consume tRPC directly.
- All procedures inherit **middleware** (auth, rate-limiting) through `createScopedAuthedProcedure` from [`packages/trpc/index.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/index.ts).

## Frequently Asked Questions

### What is the difference between tRPC routers and Hono routes in Karakeep?

The **tRPC routers** handle internal business logic with complete type safety across the client-server boundary, while **Hono routes** provide a RESTful HTTP interface for external clients. According to the karakeep-app/karakeep source code, Hono routes validate incoming HTTP requests and then call the corresponding tRPC methods via `c.var.api`, effectively acting as a thin façade over the internal tRPC layer.

### How does authentication work across Karakeep's API endpoints?

Authentication is implemented through **scoped procedures** and **middleware**. The `createScopedAuthedProcedure` helper in [`packages/trpc/index.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/index.ts) wraps procedures with authentication checks, while Hono routes in `packages/api/routes/` use `authMiddleware` to validate requests before they reach the tRPC layer. This ensures consistent security across both internal and external **API endpoints**.

### Where should I add a new feature that requires database access?

New features should be implemented in the **tRPC layer** first. Create a new router file in `packages/trpc/routers/` or extend an existing one (like [`bookmarks.ts`](https://github.com/karakeep-app/karakeep/blob/main/bookmarks.ts)), define your procedure with Zod validation, and export it from [`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts). If the feature needs to be accessible via standard HTTP clients, add a corresponding Hono route in `packages/api/routes/` that calls your new tRPC procedure.

### How does the web client maintain type safety with the backend?

The web client imports the **`AppRouter`** type from [`packages/trpc/routers/_app.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/trpc/routers/_app.ts) and uses it with `createTRPCReact<AppRouter>()`. This TypeScript generic ensures that all calls to `trpc.bookmarks.createBookmark` or similar methods are fully typed, catching errors at compile time rather than runtime. The tRPC client communicates with the backend through the `/api/trpc` endpoint defined in [`packages/api/routes/trpc.ts`](https://github.com/karakeep-app/karakeep/blob/main/packages/api/routes/trpc.ts).