@nokto-labs/relate-hono
v0.9.0
Published
Hono REST API routes for @nokto-labs/relate.
Maintainers
Readme
@nokto-labs/relate-hono
Generate a full Hono API from a Relate schema.
Links
- Repository: github.com/nokto-labs/relate
- Package source: packages/relate-hono
- Issues: github.com/nokto-labs/relate/issues
- Example: Cloudflare Worker example
Install
npm install @nokto-labs/relate @nokto-labs/relate-d1 @nokto-labs/relate-hono honoQuick Start
import { Hono } from 'hono'
import { relate } from '@nokto-labs/relate'
import { D1Adapter } from '@nokto-labs/relate-d1'
import { relateRoutes } from '@nokto-labs/relate-hono'
import { schema } from './schema'
interface Env {
DB: D1Database
}
const app = new Hono<{ Bindings: Env }>()
app.route('/', relateRoutes({
schema,
db: (c: { env: Env }) => relate({
adapter: new D1Adapter(c.env.DB),
schema,
}),
}))
export default appWhat it generates
- record CRUD routes
- optional scoped record mounts for public/admin APIs
- nested ref routes
- relationships routes
- activities routes
- lists routes
- optional schema inspection
- optional migration routes
Record, relationship, activity, and list routes are enabled by default. Meta routes (/schema and /migrate) are opt-in.
Records
| Method | Path | Description |
|--------|------|-------------|
| POST | /:plural | Create |
| PUT | /:plural | Upsert |
| GET | /:plural | List |
| GET | /:plural/:id | Get by ID |
| GET | /:plural/count | Count |
| PATCH | /:plural/:id | Update |
| DELETE | /:plural/:id | Delete |
Query params
limitoffsetorderByordercursor- filter params that map to Relate filter operators
GET /:plural/count accepts the same filter params as GET /:plural.
Scoped record routes
Use scopes when you want multiple record surfaces, such as a public read-only API plus an admin CRUD API.
relateRoutes({
schema,
db: (c) => relate({ ... }),
scopes: {
public: {
prefix: '/api',
objects: {
deal: {
operations: ['list', 'get'],
filter: { stage: { ne: 'closed_lost' } },
},
},
},
admin: {
prefix: '/api/admin',
middleware: [adminAuth],
objects: 'all',
},
},
})Scope notes
- v1 scopes apply to record routes only
- Scoped record routes must cover every schema object, or app creation fails
- If
scopesis provided, Relate does not mount the default root record routes filteris enforced on scopedlist,count, andgetroutesfilteris also enforced before scopedupdateanddelete- Scoped
filtervalues are merged with request filters, so enforced operators still apply middlewareruns only for that scope- Relationships, activities, lists, schema, and migrate routes still use the top-level
routestoggles
Nested ref routes
For each unambiguous ref field, Relate generates nested child routes automatically.
checkin: {
plural: 'checkins',
attributes: {
event: { type: 'ref', object: 'event', required: true },
guest: { type: 'ref', object: 'guest', required: true },
},
}That generates:
| Method | Path | Description |
|--------|------|-------------|
| GET | /events/:eventId/checkins | List checkins for an event |
| POST | /events/:eventId/checkins | Create a checkin with event = eventId |
| GET | /guests/:guestId/checkins | List checkins for a guest |
| POST | /guests/:guestId/checkins | Create a checkin with guest = guestId |
Flat routes still work alongside nested routes:
GET /checkins?event=evt_123Ambiguous parent-child pairs
If a child has multiple refs to the same parent object, the short path would be ambiguous.
Example:
message: {
plural: 'messages',
attributes: {
author: { type: 'ref', object: 'user', required: true },
reviewer: { type: 'ref', object: 'user', required: true },
text: { type: 'text', required: true },
},
}Relate generates explicit ref-field routes instead:
| Method | Path |
|--------|------|
| GET | /users/:userId/messages/by/author |
| POST | /users/:userId/messages/by/author |
| GET | /users/:userId/messages/by/reviewer |
| POST | /users/:userId/messages/by/reviewer |
Filtering
GET /deals?stage=won
GET /deals?value[gte]=10000&value[lt]=100000
GET /deals?stage[in]=lead,qualified,proposal
GET /people?name[like]=Ali%Operator reference
| Operator | Query shape | Example |
|----------|-------------|---------|
| equality shorthand | ?field=value | ?stage=won |
| eq | ?field[eq]=value | ?stage[eq]=won |
| ne | ?field[ne]=value | ?stage[ne]=lost |
| gt | ?field[gt]=value | ?value[gt]=1000 |
| gte | ?field[gte]=value | ?value[gte]=1000 |
| lt | ?field[lt]=value | ?value[lt]=5000 |
| lte | ?field[lte]=value | ?value[lte]=5000 |
| in | ?field[in]=a,b,c | ?stage[in]=lead,won |
| like | ?field[like]=pattern | ?name[like]=Ali% |
Value parsing
| Attribute type | Accepted query values |
|----------------|-----------------------|
| text, email, url, select, ref | strings |
| number | numeric strings like 42 or 10.5 |
| boolean | true, false, 1, 0 |
| date | ISO strings or millisecond timestamps |
Notes
likeis supported fortext,email,url,select, andrefinvalues are comma-separated in the query string- Reserved query params are
limit,offset,orderBy,order, andcursor - The same filter syntax works on record list routes and record count routes
- The route layer currently parses string, number, boolean, and date query values; typed SDK-only null filters such as
{ paymentId: { eq: null } }should still be done through directdb.*calls
Cursor pagination
GET /people?limit=20&cursor=eyJ2Ijo...Responses return:
{
"records": [],
"nextCursor": "..."
}Other route groups
Relationships
| Method | Path | Description |
|--------|------|-------------|
| POST | /relationships | Create |
| GET | /relationships | List all |
| GET | /relationships/:plural/:id | List for a record |
| PATCH | /relationships/:id | Update |
| DELETE | /relationships/:id | Delete |
Activities
| Method | Path | Description |
|--------|------|-------------|
| POST | /activities | Track |
| GET | /activities | List all |
| GET | /activities/:plural/:id | List for a record |
Lists
| Method | Path | Description |
|--------|------|-------------|
| POST | /lists | Create |
| GET | /lists | List all |
| GET | /lists/:id | Get |
| PATCH | /lists/:id | Update |
| DELETE | /lists/:id | Delete |
| POST | /lists/:id/items | Add items |
| DELETE | /lists/:id/items | Remove items |
| GET | /lists/:id/items | List items |
| GET | /lists/:id/count | Count items |
GET /lists/:id/items supports filter, limit, offset, and cursor.
GET /lists/:id/count supports filter params too.
Meta
| Method | Path | Description |
|--------|------|-------------|
| GET | /schema | Return the schema |
| POST | /migrate | Run migrations |
Meta routes are disabled by default. Enable them explicitly with routes: { schema: true, migrate: true }.
Options
relateRoutes({
schema,
db: (c) => relate({ ... }),
prefix: '/api/v1',
middleware: [auth],
scopes: {
admin: {
prefix: '/api/admin',
objects: 'all',
},
},
maxLimit: 100,
routes: { lists: false, schema: true, migrate: true },
})| Option | Purpose |
|--------|---------|
| schema | Your Relate schema |
| db | Factory that returns a Relate instance per request |
| prefix | Prefix all generated routes |
| middleware | Hono middleware to run before routes |
| scopes | Mount scoped record route surfaces with their own prefixes, middleware, and object policies |
| maxLimit | Cap ?limit= values |
| routes | Enable or disable route groups |
Errors
Relate SDK errors are mapped to HTTP responses automatically.
| Error code | Status |
|------------|--------|
| DUPLICATE_RECORD | 409 |
| RECORD_NOT_FOUND | 404 |
| RELATIONSHIP_NOT_FOUND | 404 |
| LIST_NOT_FOUND | 404 |
| VALIDATION_ERROR | 400 |
| INVALID_OPERATION | 400 |
| REF_NOT_FOUND | 400 |
| REF_CONSTRAINT | 409 |
| CASCADE_DEPTH_EXCEEDED | 500 |
Good to know
dbmust be a factory function, not a shared Relate instance- If you want hooks during API requests, create an
EventBusin your app and pass it intorelate()inside thedbfactory - Nested ref routes only exist for ref fields
- Use flat
PATCHandDELETEroutes for child records even when nested create/list routes exist - Scope object keys use schema object slugs, not plural route names
- Aggregate queries,
db.batch(), anddb.webhook()live on the Relate instance you return from thedbfactory, even thoughrelateRoutes()only generates HTTP routes for the route groups documented above
Companion packages
- Core SDK: @nokto-labs/relate
- D1 adapter: @nokto-labs/relate-d1
