npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nokto-labs/relate-hono

v0.9.0

Published

Hono REST API routes for @nokto-labs/relate.

Readme

@nokto-labs/relate-hono

Generate a full Hono API from a Relate schema.

Links

Install

npm install @nokto-labs/relate @nokto-labs/relate-d1 @nokto-labs/relate-hono hono

Quick 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 app

What 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

  • limit
  • offset
  • orderBy
  • order
  • cursor
  • 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 scopes is provided, Relate does not mount the default root record routes
  • filter is enforced on scoped list, count, and get routes
  • filter is also enforced before scoped update and delete
  • Scoped filter values are merged with request filters, so enforced operators still apply
  • middleware runs only for that scope
  • Relationships, activities, lists, schema, and migrate routes still use the top-level routes toggles

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_123

Ambiguous 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

  • like is supported for text, email, url, select, and ref
  • in values are comma-separated in the query string
  • Reserved query params are limit, offset, orderBy, order, and cursor
  • 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 direct db.* 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

  • db must be a factory function, not a shared Relate instance
  • If you want hooks during API requests, create an EventBus in your app and pass it into relate() inside the db factory
  • Nested ref routes only exist for ref fields
  • Use flat PATCH and DELETE routes 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(), and db.webhook() live on the Relate instance you return from the db factory, even though relateRoutes() only generates HTTP routes for the route groups documented above

Companion packages