Skip to content

Hexok

Hexok is a small set of classes. Import them from hexok. The Concepts pages say what each one is for.

Terminal window
npm i hexok

The package is hexok on npm. The repository is crobinson42/hexok on GitHub. TypeBox 1 schematics install as @hexok/typebox.

The repository includes an agent skill named hexok. It walks a feature from the user story through entities, use cases, events, adapters, the gateway, and where handlers run. Install it from GitHub. npx skills update refreshes that install from GitHub.

Terminal window
npx skills add crobinson42/hexok
npx skills update
Hexok primitives in the clean architecture layers Frameworks & Drivers HTTP Email Database Queue Interface Adapters Adapter Application UseCase Port EventHandler Domain Entity Schema Error Event EventCatalog

Frameworks & Drivers

HTTP · Database · Queue · Email

Interface Adapters

Adapter

The domain is the center. Dependencies point inward. The outer layer is the HTTP gateway, database, queue, and email client you choose.
Primitive Extend
Schema Schema('User', zodSchema)
Entity Entity('User', userSchema)
Port Port('UserRepository')
Adapter Adapter(UserRepository)
Mapper Mapper('mongo.User', User, mongoUserSchema)
UseCase UseCase('user.create')
Event Event('user.created', userSchema)
EventCatalog EventCatalog('domain', { userCreated })
EventHandler EventHandler('on.user.created', UserCreated)
Errors Errors('domain', { BlankName: { message: 'Name is blank' } })

EventInstance<typeof DomainEvents> is the catalog’s event objects. A use case publishes new UserCreated(...). EventMessage<typeof DomainEvents> is { key, payload } for an adapter. DomainEvents.message and DomainEvents.parse convert between them. Pass a catalog key as the second type argument to keep one entry.

The string is the token. Its type is that literal. A schema is an argument of the same call, so it cannot be left out. execute, handle, the methods on a port, fromSource, and toSource are abstract: the compiler reports a missing one on the class. parse, set, start, and stop are concrete methods you can override.

A use case takes its ports in the constructor. Hexok does not route HTTP or assemble the object graph.

UseCase.context<Ctx, Spec>() is a family factory in addition to UseCase('user.create'). It returns a factory. The optional argument is a settings object with no keys yet. Pass nothing. {} is allowed. guard and hooks are methods, not fields of that object. A missing <Ctx> type argument is a type error. Spec defaults to no required statics. execute takes a per-call context and the input. Ports stay in the constructor. Annotate input on the subclass.

.guard(fn) and .hooks(def) each return the same factory a class extends. Order does not matter. Either may be omitted. Another .guard() runs after guards already registered. A second .hooks() throws hexok: UseCase hooks are already set when the factory is created, if the first registration had at least one callback. An empty .hooks({}) does not register hooks and does not wrap execute. The call is { ctx, spec, token, input }. input is the argument passed to execute. The method receives that same value. Hexok does not validate it. call.spec is the static bag, typed as that contract. UseCase.GuardParameters<typeof ApiUseCase> is that call. UseCase.Ctx<typeof ApiUseCase> is that ctx. Pass the factory. .hooks infers the value returned from preExecute as the state argument of postExecute, onCatch, and onFinally. If preExecute is omitted, that state is void. postExecute also receives result. onFinally receives status: 'success' with result, or status: 'failure' with error. The pipeline is guard (outside try), preExecute, the method, postExecute, onCatch then rethrow, onFinally. A guard throw does not enter the hooks. A throw inside onCatch replaces the error after onFinally sees the original one. The application validates input. A schema on the static bag is application data. SchemaSource is exported for a Standard Schema or a Schema class, the same values Entity and Event accept. @hexok/typebox adapts a typebox 1 schematic to a Standard Schema. typebox(schema) infers Static. typeboxDecode(schema) infers StaticDecode and decodes. Zod and other Standard Schema libraries are passed to the primitives directly.

type ApiContext = { sessionId: string }
const ApiUseCase = UseCase.context<
ApiContext,
{ input: StandardSchemaV1; permission: string }
>()
.guard((call) => {
if (call.ctx.sessionId.length < 1 || call.spec.permission.length < 1) {
throw DomainError.Unauthorized()
}
})
.hooks({
preExecute: () => ({ started: Date.now() }),
postExecute(_call, state) {
void state.started
},
})
class FindUsers extends ApiUseCase('user.find', {
input: z.object({ query: z.string() }),
permission: 'users.read',
}) {
constructor(private readonly users: UserRepository) { super() }
async execute(
ctx: UseCase.Ctx<typeof ApiUseCase>,
input: { query: string },
): Promise<User[]> {
return this.users.search(input.query)
}
}
await new FindUsers(users).execute({ sessionId: 's' }, { query: 'ada' })