RPC, nested routing and types

Hi everyone, quick question given the attached example: - How do you avoid losing RPC typing when doing nested routing with different routes? Example /api/stores/:storeId/products. The storeId is not required in RPC because productsCollectionRoutes doesn't have the routing context of storesRoutes. - It would be less verbose to declare routes with :id instead of :productId, but I guess that's wouldn't work in RPC as I would have two ids? As you can see, I have different routes like: GET /api/stores/:storeId --> get store GET/POST /api/stores/:storeId/products --> get or create products for a store GET/PATCH/DELETE /api/products/:productId --> get, update or delete a specific product
// api.ts
const apiRoutes = new Hono<AppContext>()
.route('/stores', storesRoutes)
.route('/products', productItemRoutes)
// api.ts
const apiRoutes = new Hono<AppContext>()
.route('/stores', storesRoutes)
.route('/products', productItemRoutes)
// stores.ts
const storesRoutes = new Hono<AppContext>()
// --- authenticated ---
.use(requireAuth, requireActiveOrg, requireActiveStore)
.get('/:storeId', async (c) => {
// ...
})
.route('/:storeId/products', productsCollectionRoutes);
// stores.ts
const storesRoutes = new Hono<AppContext>()
// --- authenticated ---
.use(requireAuth, requireActiveOrg, requireActiveStore)
.get('/:storeId', async (c) => {
// ...
})
.route('/:storeId/products', productsCollectionRoutes);
// products.ts
// assume its running in the context of a store
export const productsCollectionRoutes = new Hono<OrgStoreActiveContext>()
.get('/', zValidator('query', productsFiltersSchema), async (c) => {
// ...
})
.post('/', zValidator('query', productInsertSchema), async (c) => {
// ...
})

export const productItemRoutes = new Hono<AppContext>()
.use(requireAuth, requireActiveOrg, requireActiveStore)
.get('/:productId', async (c) => {
// ...
})
// products.ts
// assume its running in the context of a store
export const productsCollectionRoutes = new Hono<OrgStoreActiveContext>()
.get('/', zValidator('query', productsFiltersSchema), async (c) => {
// ...
})
.post('/', zValidator('query', productInsertSchema), async (c) => {
// ...
})

export const productItemRoutes = new Hono<AppContext>()
.use(requireAuth, requireActiveOrg, requireActiveStore)
.get('/:productId', async (c) => {
// ...
})
8 Replies
ambergristle
ambergristle•2mo ago
i'm not sure i understand your goal or the issue you're running into
Franco Romano Losada
Franco Romano LosadaOP•2mo ago
My main issue is that as I have my productsCollectionRoutes declared separated from the storesRoutes, I don't get the type safety params for the :storeId. I was wondering if there's a workaround to that.
ambergristle
ambergristle•2mo ago
i don't know the logic behind your app/api, so i'm still not sure i understand you have routes like this
GET /api/stores/:storeId
? /api/stores/:storeId/products
GET /api/products
POST /api/products
GET /api/stores/:storeId
? /api/stores/:storeId/products
GET /api/products
POST /api/products
are you talking about handling a route like this?
/api/stores/:storeId/products/:productId
/api/stores/:storeId/products/:productId
Franco Romano Losada
Franco Romano LosadaOP•2mo ago
Sorry, I was out. Yes, those are the routes.
are you talking about handling a route like this?
No, my resource route is under the resource route: GET api/products/:productId The thing is that this route doesn't infer the type in RPC: GET /api/stores/:storeId/products Just wondering if it was possible to infer it somehow by specifying some generic type in the new Hono<...> of the productsCollectionRoutes declaration
ambergristle
ambergristle•2mo ago
this is what i would expect from a RESTful perspective:
GET /api/stores/:storeId - specific store
GET /api/stores/:storeId/products - all products at specific store
GET /api/stores/:storeId/products/:productId - specific product at specific store
GET /api/products - all products, independent of store
GET /api/products/:productId - specific product, independent of store
GET /api/stores/:storeId - specific store
GET /api/stores/:storeId/products - all products at specific store
GET /api/stores/:storeId/products/:productId - specific product at specific store
GET /api/products - all products, independent of store
GET /api/products/:productId - specific product, independent of store
you might not need/have all of these routes but if i understand correctly, you have a route /api/products/:productId where you're doing something with products for a specific store. it would be more intuitive to do that at /api/stores/:storeId/products/:productId then it's clear from the url what all the endpoint applies to i think maybe i'm starting to understand your issue though the problem is with how you've set up stores
const storeProducts = new Hono()
.get('/', /** */)
.post('/', /** */)

const stores = new Hono()
.get('/:storeId', /** */)
.route('/:storeId/products', storeProducts);
const storeProducts = new Hono()
.get('/', /** */)
.post('/', /** */)

const stores = new Hono()
.get('/:storeId', /** */)
.route('/:storeId/products', storeProducts);
i'm not sure why, but this is breaking the typing instead, try this
const storeProducts = new Hono()
// .get('/', async (c) => {})
.get('/', /** */)
.post('/', /** */)

const store = new Hono()
// .get('/', async (c) => {})
.route('/products', storeProducts)

const stores = new Hono()
// .get('/', async (c) => {})
.route('/:storeId', store)
const storeProducts = new Hono()
// .get('/', async (c) => {})
.get('/', /** */)
.post('/', /** */)

const store = new Hono()
// .get('/', async (c) => {})
.route('/products', storeProducts)

const stores = new Hono()
// .get('/', async (c) => {})
.route('/:storeId', store)
or, if you really don't want the intermediate layer
const storeProducts = new Hono()
.baseUrl('/products')
.get('/', /** */)
.post('/', /** */)

const stores = new Hono()
.route('/:storeId', storeProducts)
const storeProducts = new Hono()
.baseUrl('/products')
.get('/', /** */)
.post('/', /** */)

const stores = new Hono()
.route('/:storeId', storeProducts)
tl;dr - hono should infer param types the way you want, but the way you've structured your routes is breaking the typing somehow the issue arises specifically from this
app.route('/:dynamicParam/staticParam')
app.route('/:dynamicParam/staticParam')
i.e., routing to a path that has a dynamic parameter, and is followed by a static parameter. if the static paremeter precedes the dynamic paremeter, there's no problem (.e., app.route('/static/:dynamic') works fine) does that make sense? has something to do with how hono's type system constructs the route type
Franco Romano Losada
Franco Romano LosadaOP•2mo ago
okay, that makes sense. thanks a lot!
Agustín
Agustín•7d ago
Hi there 👋 I'm facing the same issue, is there a way to do it now?
ambergristle
ambergristle•7d ago
is there a way to do what now? i don't think the type edge-case has been addressed, but you should be good w a restful architecture

Did you find this page helpful?