kshdotdev/hono-crud(opens in a new tab)
Language: TypeScript
A powerful CRUD generator for Hono with Zod validation and automatic OpenAPI documentation.
- stars
- 10
- watchers
- 0
- forks
- 4
- open issues
- 16
- Last updated
- 10/03/2026
Language: TypeScript
A powerful CRUD generator for Hono with Zod validation and automatic OpenAPI documentation.
Type-safe CRUD generator for Hono — Zod validation, automatic OpenAPI docs,
and an edge-ready feature set, all from a single model definition.
Install the core package plus the adapter you need (and Swagger UI, if you want docs):
npm install hono-crud @hono-crud/memory @hono-crud/swagger hono zod
Peer dependencies: hono >= 4.11.7 <5 and zod >= 4.0.0 are required.
hono-crud is published as a small core plus focused add-on packages, so you only install what you use:
| Package | Purpose |
|---|---|
hono-crud |
Core: defineModel, registerCrud, fromHono, endpoint classes, auth, logging, events, encryption, serialization, audit, versioning, multi-tenant, api-version |
@hono-crud/memory |
In-memory CRUD adapter (tests, demos) |
@hono-crud/drizzle |
Drizzle ORM CRUD adapter |
@hono-crud/prisma |
Prisma CRUD adapter |
@hono-crud/swagger |
Swagger UI + ReDoc documentation pages (swaggerUI, redocUI, docsIndex) |
@hono-crud/scalar |
Scalar API reference documentation page (scalarUI) |
@hono-crud/cache |
Caching mixins and storage backends |
@hono-crud/rate-limit |
Rate limiting middleware and storage backends |
@hono-crud/idempotency |
Idempotency middleware and storage backends |
@hono-crud/mcp |
Expose your CRUD resources as Model Context Protocol (MCP) tools for AI agents |
Health check routes (createHealthRoutes) live in core, importable from the hono-crud/health subpath.
The samples in the rest of this README build on this setup:
import { Hono } from 'hono'; import { z } from 'zod'; import { fromHono, registerCrud, defineModel, defineMeta } from 'hono-crud'; import { swaggerUI } from '@hono-crud/swagger'; import { MemoryCreateEndpoint, MemoryReadEndpoint, MemoryUpdateEndpoint, MemoryDeleteEndpoint, MemoryListEndpoint, } from '@hono-crud/memory'; // 1. Define your schema const UserSchema = z.object({ id: z.uuid(), email: z.email(), name: z.string().min(1), role: z.enum(['admin', 'user']), }); // 2. Create model + meta const UserModel = defineModel({ tableName: 'users', schema: UserSchema, primaryKeys: ['id'], }); const userMeta = defineMeta({ model: UserModel }); // 3. Define endpoints class UserCreate extends MemoryCreateEndpoint { _meta = userMeta; schema = { tags: ['Users'], summary: 'Create a user' }; } class UserList extends MemoryListEndpoint { _meta = userMeta; schema = { tags: ['Users'], summary: 'List users' }; filterFields = ['role']; searchFields = ['name', 'email']; } class UserRead extends MemoryReadEndpoint { _meta = userMeta; schema = { tags: ['Users'], summary: 'Get a user' }; } class UserUpdate extends MemoryUpdateEndpoint { _meta = userMeta; schema = { tags: ['Users'], summary: 'Update a user' }; allowedUpdateFields = ['name', 'role']; } class UserDelete extends MemoryDeleteEndpoint { _meta = userMeta; schema = { tags: ['Users'], summary: 'Delete a user' }; } // 4. Wire it up const app = fromHono(new Hono()); const userEndpoints = { create: UserCreate, list: UserList, read: UserRead, update: UserUpdate, delete: UserDelete, }; registerCrud(app, '/users', userEndpoints); // 5. OpenAPI docs app.doc('/openapi.json', { openapi: '3.1.0', info: { title: 'My API', version: '1.0.0' }, }); app.get('/docs', swaggerUI({ specUrl: '/openapi.json' })); export default app;
This generates:
| Method | Route | Description |
|---|---|---|
POST |
/users |
Create a user |
GET |
/users |
List users (with filtering, search, pagination) |
GET |
/users/:id |
Get a user by ID |
PATCH |
/users/:id |
Update a user |
DELETE |
/users/:id |
Delete a user |
hono-crud supports four ways to define endpoints. All produce classes compatible with registerCrud() and can be mixed.
| Pattern | Best For | Style |
|---|---|---|
| Class-based | Complex logic, database adapters | class UserList extends MemoryListEndpoint { ... } |
| Functional | Quick setup | createList({ meta, filterFields: ['role'] }, MemoryListEndpoint) |
| Builder | Readable chains | crud(meta).list().filter('role').build(MemoryListEndpoint) |
| Config-based | Declarative, all-in-one | defineEndpoints({ meta, list: { ... } }, MemoryAdapters) |
import { defineEndpoints } from 'hono-crud'; import { crud } from 'hono-crud/builder'; import { createList } from 'hono-crud/functional'; import { MemoryAdapters } from '@hono-crud/memory'; // Functional const FunctionalUserList = createList( { meta: userMeta, filterFields: ['role'], searchFields: ['name'] }, MemoryListEndpoint ); // Builder const BuilderUserList = crud(userMeta) .list() .filter('role') .search('name') .pagination(20, 100) .build(MemoryListEndpoint); // Config-based (all endpoints at once) const endpoints = defineEndpoints({ meta: userMeta, create: { openapi: { tags: ['Users'], summary: 'Create user' } }, list: { filtering: { fields: ['role'] }, search: { fields: ['name'] } }, read: {}, update: { fields: { allowed: ['name', 'role'] } }, delete: {}, }, MemoryAdapters); registerCrud(app, '/people', endpoints);
See docs/alternative-api-patterns.md for the full reference.
Apps built with fromHono accumulate Hono's route schema, so Hono's typed fetch client works out of the box — no generated SDK:
import { hc } from 'hono/client'; const routes = registerCrud(app, '/users', { list: UserList, create: UserCreate, read: UserRead }); export type AppType = typeof routes; const client = hc<AppType>('https://api.example.com'); const res = await client.users[':id'].$get({ param: { id: 'u1' } }); if (res.status === 200) { const { result } = await res.json(); // typed row }
Class routes contribute their schema (declare it with satisfies OpenAPIRouteSchema), CRUD verbs contribute the envelope they document, and registerCrudResources folds several resources into one type. See docs/typed-client.md.
Bindings are injected per request (c.set('db', drizzle(c.env.DB)) + createStorageMiddleware with KVCacheStorage / KVRateLimitStorage / DrizzleAuditLogStorage), managed timestamps and filter values follow each column's declared representation, and background work goes through waitUntil. See docs/cloudflare.md for the wrangler template, the column conventions and the D1 limits.
Zero-config, perfect for prototyping and tests (npm install @hono-crud/memory):
import { MemoryCreateEndpoint, MemoryListEndpoint /* ... */ } from '@hono-crud/memory';
npm install @hono-crud/drizzle drizzle-orm drizzle-zod. Use createDrizzleCrud for minimal boilerplate:
import { createDrizzleCrud, type DrizzleDatabaseConstraint } from '@hono-crud/drizzle'; declare const drizzleDb: DrizzleDatabaseConstraint; // your drizzle instance const User = createDrizzleCrud(drizzleDb, userMeta); class CreateUser extends User.Create { schema = { tags: ['Users'], summary: 'Create user' }; } class ListUsers extends User.List { schema = { tags: ['Users'], summary: 'List users' }; filterFields = ['role']; }
Or set db directly on each endpoint class:
import { DrizzleListEndpoint, type DrizzleDatabaseConstraint } from '@hono-crud/drizzle'; declare const drizzleDb: DrizzleDatabaseConstraint; // your drizzle instance class DrizzleUserList extends DrizzleListEndpoint { _meta = userMeta; db = drizzleDb; filterFields = ['role']; }
npm install @hono-crud/prisma @prisma/client pluralize fastest-levenshtein:
import { PrismaListEndpoint, type PrismaClient } from '@hono-crud/prisma'; declare const prismaClient: PrismaClient; // your generated Prisma client class PrismaUserList extends PrismaListEndpoint { _meta = userMeta; prisma = prismaClient; filterFields = ['role']; }
See docs/database-adapters.md for complete setup guides.
Built-in JWT and API Key middleware with composable guards:
import { env } from 'hono/adapter'; import { anyOf, createJWTMiddleware, requireAuthenticated, requireOwnership, requireRoles, type AuthEnv, } from 'hono-crud/auth'; // Auth-aware endpoints share the app's Env type const securedApp = fromHono(new Hono<AuthEnv>()); class SecuredUserCreate extends MemoryCreateEndpoint<AuthEnv> { _meta = userMeta; } class SecuredUserDelete extends MemoryDeleteEndpoint<AuthEnv> { _meta = userMeta; } // JWT middleware — read the secret per request via env() (edge-safe) securedApp.use('/api/*', (c, next) => { const { JWT_SECRET } = env<{ JWT_SECRET: string }>(c); return createJWTMiddleware({ secret: JWT_SECRET, issuer: 'my-app' })(c, next); }); // Guards on specific endpoints registerCrud(securedApp, '/users', { create: SecuredUserCreate, delete: SecuredUserDelete }, { middlewares: [requireAuthenticated()], endpointMiddlewares: { delete: [requireRoles('admin')], }, }); // Composable guards securedApp.use('/admin/*', anyOf( requireRoles('admin'), requireOwnership((ctx) => ctx.req.param('id')) ));
See docs/authentication.md for JWT, API Key, guards, and better-auth integration.
import { withCache, MemoryCacheStorage } from '@hono-crud/cache'; import { createStorageMiddleware } from 'hono-crud/storage'; // Wire storage (recommended: per-request injection, edge-safe) app.use('*', createStorageMiddleware({ cacheStorage: new MemoryCacheStorage() })); // The registrar calls setContext(c) before handle() — overrides are parameterless class CachedUserRead extends withCache(MemoryReadEndpoint) { _meta = userMeta; cacheConfig = { ttlSeconds: 300, perUser: false }; override async handle(): Promise<Response> { const cached = await this.getCachedResponse(); if (cached) return this.successWithCache(cached); const response = await super.handle(); if (response.status === 200) { const data = (await response.clone().json()) as { result: unknown }; await this.setCachedResponse(data.result); } return response; } }
See docs/caching.md.
import { createRateLimitMiddleware, MemoryRateLimitStorage } from '@hono-crud/rate-limit'; import { createStorageMiddleware } from 'hono-crud/storage'; app.use('*', createStorageMiddleware({ rateLimitStorage: new MemoryRateLimitStorage() })); app.use('/api/*', createRateLimitMiddleware({ limit: 100, windowSeconds: 60, keyStrategy: 'ip', }));
import { createLoggingMiddleware, MemoryLoggingStorage } from 'hono-crud/logging'; import { createStorageMiddleware } from 'hono-crud/storage'; app.use('*', createStorageMiddleware({ loggingStorage: new MemoryLoggingStorage() })); app.use('*', createLoggingMiddleware({ redactHeaders: ['authorization', 'cookie'], redactBodyFields: ['password'], }));
See docs/logging.md.
Expose the CRUD resources you already register as Model Context Protocol tools (npm install @hono-crud/mcp @modelcontextprotocol/sdk). Tool calls are re-dispatched through the same Hono app, so they run the identical pipeline — auth, validation, hooks, serialization — as your REST API.
import { createCrudMcp } from '@hono-crud/mcp'; const mcp = createCrudMcp(app, { name: 'my-api', version: '1.0.0' }); mcp.resource('/users', userEndpoints); // or: createCrudMcp(app, { ..., auto: true }) app.all('/mcp', mcp.handler());
This generates users_list, users_read, users_create, users_update, and users_delete tools, each with an input schema derived from the endpoint's Zod schema. See docs/mcp.md.
Every CRUD endpoint defaults to a small, predictable response envelope so consumers always know where to find result and error:
// Success — single item (Create / Read / Update / Restore / Upsert / Clone / …) { "success": true, "result": { "id": "…", … } } // Success — list / search (with pagination metadata) { "success": true, "result": [ … ], "result_info": { "page": 1, "per_page": 20, … } } // Error — produced by `ApiException`s thrown from the endpoint or by // `createErrorHandler` for everything else { "success": false, "error": { "code": "NOT_FOUND", "message": "…", "details": … } } // Error — validation (request schema failure): same envelope, status 400 { "success": false, "error": { "code": "VALIDATION_ERROR", "message": "Validation failed", "details": [{ "path": "name", "message": "Required", "code": "invalid_type" }] } }
Validation failures use the same envelope: fromHono installs the canonical validation hook as the router's defaultHook, so every request-schema failure responds 400 with code: "VALIDATION_ERROR" and per-issue details. To supply your own hook, pass a pre-configured router — fromHono(new OpenAPIHono({ defaultHook })) (OpenAPIHono from @hono/zod-openapi) — and it wins.
Thrown ApiExceptions serialize to this envelope even without any error-handler wiring (via their getResponse()); wiring createErrorHandler with app.onError is what extends the fully-uniform wire shape to non-ApiException errors (and adds requestId enrichment).
If your house API standard prefers a different shape — RFC 7807 Problem Details, JSON:API { data, meta }, or any custom envelope — pass responseEnvelope to registerCrud. The two functions are the final formatting step before the response body is serialised:
import type { ResponseEnvelope } from 'hono-crud'; const envelope: ResponseEnvelope = { success: (result, info) => info ? { data: result, meta: info } : { data: result }, error: (err) => ({ errors: [{ status: err.code, title: err.message, source: err.details }], }), }; registerCrud(app, '/enveloped-users', userEndpoints, { responseEnvelope: envelope });
The info argument is the pagination metadata for list/search responses; it's undefined for single-item responses, so a single envelope works across the whole CRUD surface.
For errors, the envelope composes with the existing mappers chain on createErrorHandler. The order is fixed:
mappers[] (and the built-in zodErrorMapper) transform the raw Error into a structured ApiException ({ code, message, details? }).responseEnvelope.error(...) wraps that structured object into the final response body.import { ConflictException, createErrorHandler, type ErrorMapper, type ResponseEnvelope, } from 'hono-crud'; declare const envelope: ResponseEnvelope; // from the previous sample const prismaMapper: ErrorMapper = (err) => { if ((err as { code?: string }).code === 'P2002') { return new ConflictException('Duplicate key', { /* … */ }); } }; app.onError(createErrorHandler({ mappers: [prismaMapper], // Handler-level default — applies to errors that propagate to onError // (i.e. anything that's not already an ApiException). The per-route // envelope passed to `registerCrud(app, path, endpoints, { responseEnvelope })` // always wins. responseEnvelope: envelope, }));
This split lets you keep your domain-error mappers (Prisma codes, Drizzle constraint violations, …) unchanged and layer a custom shape on top — no response-rewriting middleware required.
When responseEnvelope is omitted (the default), the response body is byte-identical to pre-0.10.0 — existing consumers see no behaviour change.
softDelete: true in model, ?withDeleted=true, restore endpointhasOne, hasMany, belongsTo with ?include=posts,profile; author circular graphs in one defineModels call with auto-populated relation schemas/tables and compile-checked sibling references?fields=id,name,emailSee docs/advanced-features.md for examples of every feature.
The 'hono-crud' root barrel owns the CRUD core (models, endpoints, registrar, exceptions, OpenAPI utilities). Every feature family lives only on its tree-shakeable subpath:
import { createJWTMiddleware, requireRoles } from 'hono-crud/auth'; import { createLoggingMiddleware, MemoryLoggingStorage } from 'hono-crud/logging'; import { createStorageMiddleware, type StorageEnv } from 'hono-crud/storage'; import { multiTenant } from 'hono-crud/multi-tenant'; import { createAuditLogger, MemoryAuditLogStorage } from 'hono-crud/audit'; import { VersionManager, MemoryVersioningStorage } from 'hono-crud/versioning'; import { CrudEventEmitter, registerWebhooks } from 'hono-crud/events'; import { encryptFields, decryptFields, StaticKeyProvider } from 'hono-crud/encryption'; import { applyProfile, type SerializationProfile } from 'hono-crud/serialization'; import { apiVersion, getApiVersion } from 'hono-crud/api-version'; import { createCreate, createList } from 'hono-crud/functional'; import { crud } from 'hono-crud/builder'; import { createHealthRoutes } from 'hono-crud/health'; import { getWaitUntil } from 'hono-crud/cloudflare';
Idempotency ships as its own package:
import { createIdempotencyMiddleware, MemoryIdempotencyStorage } from '@hono-crud/idempotency'; app.use('/api/*', createIdempotencyMiddleware());
npm install @hono-crud/swagger @hono-crud/scalar:
import { redocUI } from '@hono-crud/swagger'; // swaggerUI already imported in the Quick Start import { scalarUI } from '@hono-crud/scalar'; // OpenAPI spec app.doc('/openapi.json', { openapi: '3.1.0', info: { title: 'My API', version: '1.0.0' }, }); // Documentation UIs app.get('/docs', swaggerUI({ specUrl: '/openapi.json' })); app.get('/redoc', redocUI({ specUrl: '/openapi.json' })); app.get('/reference', scalarUI({ specUrl: '/openapi.json' }));
See the examples/ directory for complete working applications:
MIT - Kauan Guesser