typescriptexpressdenohono

How do I use the Hono router to match a parameter + file extension without the ext being included in the extracted value?


I'm exploring with TypeScript, Deno & the Hono web app framework, but I'm struggling to put together an elegant solution to something that is simple in Express. There doesn't seem to be much documentation on this topic (that I can find).

I would like a route with a dynamic parameter where the value is suffixed with .json, but where the file extension isn't included in the extracted parameter value. The below examples should add some clarity.

The following examples fetch GET /1234.json:

// Express

app.get('/:id.json', req => {
  console.log( req.params.id ); // 1234 (✓)
});
// Hono

app.get('/:id.json', req => { // '.json' is part of param name
  console.log( req.param('id.json') ); // 1234.json (✗)
});

// or

app.get('/:id{.+\\.json}', req => {
  console.log( req.param('id') ); // 1234.json (✗)
});

// or

app.get('/:id{.+\\.json}', req => {
  const id = req.param('id').replace(/\.json$/, '');
  console.log( id ); // 1234 (✓)
});

The last Hono example is functional, but it isn't as elegant where it's prone to code duplication issues. Is anyone able to suggest a solution closer to the implementation in Express?


Solution

  • As discussed in the question comments, it doesn't seem like Hono natively supports what you want (path parameters match entire path segments—everything between a pair of slashes). You'll have to handle this yourself using something like your last example. If there are multiple routes which need this sort of thing you could factor out the logic and reuse it.

    Here's an example of how it could be done via reusable middleware which sets a context variable:

    const jsonExtension = '.json'
    const stripJSONExtensionFromID = createMiddleware<{
      Variables: { id: string }
    }>(async (context, next) => {
      const id = context.req.param(`id${jsonExtension}`)
      if (id !== undefined) {
        context.set('id', id.slice(0, 0 - jsonExtension.length))
      }
      await next()
    })
    
    app.get(`/:id.json`, stripJSONExtensionFromID, context => {
      const id = context.get('id')
      return context.text(id)
    })
    

    And here's a slightly more general version which allows arbitrary parameter names, in case that's something you need:

    const jsonExtension = '.json'
    const stripJSONExtensionFromParam = <N extends string>(parameterName: N) =>
      createMiddleware<{
        Variables: Record<N, string>
      }>(async (context, next) => {
        const arg = context.req.param(`${parameterName}${jsonExtension}`)
        if (arg !== undefined) {
          context.set(parameterName, arg.slice(0, 0 - jsonExtension.length))
        }
        await next()
      })
    
    app.get(`/:id.json`, stripJSONExtensionFromParam('id'), context => {
      const id = context.get('id')
      return context.text(id)
    })
    

    The specific extension to strip could also become a parameter, but in chat you said you only care about .json, so that seems like overkill.