Skip to content

Support for defining custom context helper methods #3965

Open
@cheese-mountain

Description

@cheese-mountain

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>
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions