Open
Description
What is the feature you are proposing?
What is the feature you are proposing?
I'm proposing the ability to define and register custom helper methods that will be directly accessible on the Context object. This would enable developers to create reusable utility functions that have access to the context instance through this
binding, improving code organization and developer experience.
Currently, utility functions need to be imported separately and explicitly passed the context, or set as variables on the context. Having first-class support for helper methods would make the code more elegant and maintainable.
Why is this feature valuable?
- Improves code organization by allowing related functionality to be grouped as context methods
- Reduces boilerplate by eliminating the need to repeatedly pass context to utility functions
- Provides better TypeScript integration with proper
this
typing - Makes middleware and route handlers cleaner and more readable
- Follows patterns common in other frameworks where context/request objects can be extended
Current Implementation vs Proposed Feature
Current Implementation:
type Env = {
Bindings: {
SUPABASE_URL: string;
SUPABASE_ANON_KEY: string;
},
Variables: {
'supabase-token': string
supabase: TypedSupabaseClient
log: (str: unknown) => void
}
}
const factory = createFactory<Env>()
export const Router = factory.createApp
export const createMiddleware = factory.createMiddleware
export type Context = HonoContext<Env>
// In route handlers:
app.get('/users', async (c) => {
console.log(`${c.req.method} ${c.req.path}: Fetching users`); // Commonly replaced with a log helper method
const supabase = c.get('supabase');
const { data, error } = await supabase.from('users').select('*'); // Also could be replaced with helper method
if (error) {
console.log(`${c.req.method} ${c.req.path}: ${error}`); // Same here
return c.json({ error: 'Failed to fetch users' }, 500);
}
return c.json(data);
})
Proposed Feature Implementation:
const helpers = {
log: function(this: Context, message: string, level: 'info'| 'error' = 'info') {
console[level](`[${this.req.method}] ${this.req.path}: ${message}`);
},
useSupabase: async function(this: Context, callback: (supabase: TypedSupabaseClient) => Promise<any>){
const supabase = this.get('supabase')
const { data, error } = await callback(supabase)
if (error) {
this.log(error)
return null
}
return data
}
}
type Env = { ..., Helpers: typeof helpers }
const factory = createFactory<Env>({ defaultAppOptions: { helpers } })
// In route handlers:
app.get('/users', async (c) => {
c.log('Fetching users');
const users = await c.useSupabase(client => client.from('users').select('*'));
return users ? c.json(users) : c.json({ error: 'Failed to fetch users' }, 500);
})
Possible Implementation
Working possible implementation (which I'm using myself)
// Types.ts, extend Env with helpers
export type Bindings = object
export type Variables = object
export type Helpers = Record<string, any>
export type BlankEnv = {}
export type Env = {
Bindings?: Bindings
Variables?: Variables
Helpers?: Helpers
}
// hono-base.ts, extend HonoOptionswith helpers
export type HonoOptions<E extends Env> = {
...
helpers?: E['Helpers']
}
// Context.ts
// Rename class & assign helpers in constructor if provided
export class ContextBase<
E extends Env = any,
P extends string = any,
I extends Input = {}
> {
...
constructor(req: Request, options?: ContextOptions<E>) {
this.#rawRequest = req
if (options) {
this.#executionCtx = options.executionCtx
this.env = options.env
this.#notFoundHandler = options.notFoundHandler
this.#path = options.path
this.#matchResult = options.matchResult
if (options.helpers) {
Object.assign(this, options.helpers)
}
}
}
...
}
// Extend ContextBase with Context to typesafely include helper methods
type HelperMethods<E extends Env = Env> =
E['Helpers'] extends undefined ? {} :
IsAny<E['Helpers']> extends true ? {} :
E['Helpers'] extends Helpers ? NonNullable<E['Helpers']> : {}
export type Context<
E extends Env = any,
P extends string = any,
I extends Input = {},
> = HelperMethods<E> & ContextBase<E, P, I>
export const Context = ContextBase as (
new <E extends Env = any, P extends string = any, I extends Input = {}>
(...args: ConstructorParameters<typeof ContextBase<E, P, I>>) => Context<E, P, I>
)