uniku
Guides

Integrations

Runnable starting points for Hono, Drizzle ORM v1, and Effect v4.

The examples below add Nanoid request IDs to Hono, binary ULIDs to a Drizzle schema, and prefixed TypeIDs to an Effect service. Each is self-contained and runs with the command below it.

Hono request IDs with Nanoid

Hono's request ID middleware accepts a synchronous generator. When a request does not already carry a valid X-Request-Id header, this example generates a compact Nanoid, stores it in the request context, and returns it in the response header.

hono-nanoid.ts
import { expect, test } from 'bun:test'import { Hono } from 'hono'import { requestId } from 'hono/request-id'import { nanoid } from 'uniku/nanoid'const app = new Hono()app.use(  '*',  requestId({    generator: () => nanoid(),  }),)app.get('/', (context) => context.json({ requestId: context.get('requestId') }))// Example: requestId = 'QC-h76CxpXPmlj9e_1nAG'test('uses a Nanoid as the Hono request ID', async () => {  const response = await app.request('/')  const body: unknown = await response.json()  expect(body).toEqual({ requestId: expect.any(String) })  if (!body || typeof body !== 'object' || !('requestId' in body) || typeof body.requestId !== 'string') {    throw new Error('Hono did not return a request ID')  }  console.log('body.requestId:', body.requestId)  expect(nanoid.isValid(body.requestId)).toBe(true)  expect(response.headers.get('X-Request-Id')).toBe(body.requestId)})

Run it with pnpm --filter @uniku/examples example:hono.

Drizzle ORM v1 with binary ULIDs

The binaryUlid custom type exposes a string to application code while declaring a PostgreSQL bytea column. Its Drizzle codec converts ULID strings to Uint8Array parameters and selected bytes back to canonical ULID strings.

The column's $defaultFn generates a ULID when an insert omits id. Both sides of the foreign key remain 16-byte values in PostgreSQL, so the join compares their binary representation while the returned eventId is already a string. The example uses an in-memory PGlite database, so it needs no external PostgreSQL server.

drizzle-ulid.ts
import { expect, test } from 'bun:test'import { PGlite } from '@electric-sql/pglite'import { eq, sql } from 'drizzle-orm'import { customType, pgTable, text } from 'drizzle-orm/pg-core'import { drizzle } from 'drizzle-orm/pglite'import { ulid } from 'uniku/ulid'const binaryUlid = customType<{ data: string; driverData: Uint8Array }>({  dataType: () => 'bytea',  codec: 'bytea',})const events = pgTable('events', {  id: binaryUlid()    .primaryKey()    .$defaultFn(() => ulid()),  name: text().notNull(),})const eventDeliveries = pgTable('event_deliveries', {  eventId: binaryUlid('event_id')    .notNull()    .references(() => events.id),  target: text().notNull(),})test('generates and stores a binary ULID with Drizzle', async () => {  const client = new PGlite()  const db = drizzle({    client,    codecs: {      bytea: {        normalize: (value: Uint8Array) => ulid.fromBytes(value),        normalizeInJson: (value: string) => ulid.fromBytes(Uint8Array.fromBase64(value)),        normalizeParam: (value: string) => ulid.toBytes(value),      },    },  })  try {    await db.execute(sql`      create table events (        id bytea primary key,        name text not null      )    `)    await db.execute(sql`      create table event_deliveries (        event_id bytea not null references events(id),        target text not null      )    `)    const [event] = await db.insert(events).values({ name: 'account.created' }).returning()    if (!event) throw new Error('The inserted event was not returned')    await db.insert(eventDeliveries).values({ eventId: event.id, target: 'analytics' })    const [delivery] = await db      .select({        eventId: events.id,        storedBytes: sql<number>`octet_length(${events.id})`,        target: eventDeliveries.target,      })      .from(events)      .innerJoin(eventDeliveries, eq(events.id, eventDeliveries.eventId))    if (!delivery) throw new Error('The joined delivery was not returned')    // Example: delivery.eventId = '01KXJP51P435NADKZ683PNVK1R'    console.log('delivery.eventId:', delivery.eventId)    expect(delivery.eventId).toBe(event.id)    expect(typeof delivery.eventId).toBe('string')    expect(ulid.isValid(delivery.eventId)).toBe(true)    expect(delivery.storedBytes).toBe(16)    expect(delivery.target).toBe('analytics')    expect(event.name).toBe('account.created')  } finally {    await client.close()  }})

Run it with pnpm --filter @uniku/examples example:drizzle.

Drizzle v1 release candidate

The example pins Drizzle ORM 1.0.0-rc.4. Codecs are configured per PostgreSQL type, so this client treats every bytea column as a binary ULID. The v1 line is still a release candidate, so check its release notes before changing that version.

Effect v4 services with TypeID

An Effect service keeps ID generation injectable without leaking framework types into the generated value. This service exposes a traced next() effect and returns TypeID strings with a stable user_ prefix.

effect-typeid.ts
import { expect, test } from 'bun:test'import { Context, Effect, Layer } from 'effect'import { typeid } from 'uniku/typeid'class UserIds extends Context.Service<  UserIds,  {    readonly next: () => Effect.Effect<string>  }>()('examples/UserIds') {  static readonly layer = Layer.effect(    UserIds,    Effect.gen(function* () {      const next = Effect.fn('UserIds.next')(function* () {        return yield* Effect.sync(() => typeid('user'))      })      return UserIds.of({ next })    }),  )}const program = Effect.gen(function* () {  const userIds = yield* UserIds  return yield* userIds.next()})test('generates a prefixed TypeID through an Effect service', () => {  const userId = Effect.runSync(program.pipe(Effect.provide(UserIds.layer)))  // Example: userId = 'user_01kxjp5jcqej3tm5bgqj6mz05r'  console.log('userId:', userId)  expect(typeid.isValid(userId)).toBe(true)  expect(typeid.prefix(userId)).toBe('user')})

Run it with pnpm --filter @uniku/examples example:effect.

Effect v4 prerelease

This example pins Effect 4.0.0-beta.93. Effect v4 is still prerelease software, so check its release notes before changing that version.

On this page