Router

Router is the brains of your operations. It creates your API definitions for you and packs some useful utilities along the way!

Declaration

The declaration of router is straightforward. You pass a configuration object which can contain the following properties:

ArgumentTypeStatusDescription
namestringRequiredUsed for request deduping, key encoding
routesreadonly [Procedures, or other custom methods]RequiredRoutes definitions, calls you’ll be executing
interceptorsInterceptorsOptionalUtility functions ran on every matching event
adaptersAdaptersOptionalIntegrations and Transformers

Here’s how it looks in practice:

import { api } from '@hulla/api'

// can pass a function reference, or just define it inside the call definition
const getAllUsers = () => db.from('users').select('*')
const getPokemon = (name: string) => fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)

const a = api()
export const exampleAPI = a.router({
  name: 'example', // first the router name!
  routes: [ // then the routes - can be the followng
    a.procedure('all', getAllUsers), // procedure
    a.procedure('foo', () => 'foo'),
    a.procedure('poke', getPokemon),
    // ...etc
  ]
})
const a = api({ context, methods })import { api } from @hulla/apiInitializationContextCustom methodsRouterRouter nameInterceptorsRoutesProcedure routeProcedure routeProcedure routeCustom method routeexport const usersAPI = a.router({ name: 'usersAPI', routes: [ a.procedure(...), a.procedure(...), a.procedure(...), a.request.get(...) ], interceptors, adapters,})Adapters

Usage

You should always export your ready API definition as the result of the .router() call

// path: src/api/users.ts
import { api } from '@hulla/api'

const getAllUsers = () => db.from('users').select('*')
const getUserById = (id: string) => db.from('users').where('id', '==', id).selectFirst('*')

const a = api()
export const usersAPI = a.router({
  name: 'users',
  routes: [
    a.procedure('all', getAllUsers),
    a.procedure('byId', getUserById),
  ]
})

and then import it in your code (server/client/wherever)

// path: any-where-you-want.ts
import { usersAPI } from '@/src/api/users' // (points to your exported API creation)

const allUsers = await usersAPI.call('all') // { id: string, name: string }[] ✅
const john = await usersAPI.call('byId', '0') // { id: '0', name: 'John' } ✅

// @ts-expect-error purposely bad call - super easy to miss (number arg vs string arg)
const badCall = await usersAPI.call('byId', 0) // ❌ type error
//                                     ^ expected 'string', got 'number'
RouterRouter nameInterceptorsRoutesProcedure routeProcedure routeProcedure routeCustom method routeexport const usersAPI = a.router({ name: 'usersAPI', routes: [ a.procedure(...), a.procedure(...), a.procedure(...), a.request.get(...) ], interceptors, adapters,})AdaptersPage / ComponentimportsCalls a procedureusersAPI.call('getUserById', '123')passed argsProcedure route getUserByIdreturns a value

Return type (API)

Apart from .call which will be your most frequent use of a created router API, you also have access also to some nice utility methods/properties.

Here is the exact return type after calling your a.router()

Best practices

Router doesn’t really have any gotchas. These are more of general suggestions of tested by time best practices.

Project structure

This way when you import your API accross the application, you don’t import unnecessary calls and heavens-forbid unnecessary dependencies. Usually it’s good idea to separate routers by their subject matter.

// even though posts have relation to users (i.e. user has multiple posts). You probably
// don't need all the API calls related to posts when fetching a user's profile.

// path: src/api/users.ts
export const usersAPI = a.router({
  name: 'users',
  routes: [
    /*...*/
  ],
})
// path: src/api/posts.ts
export const postsAPI = a.router({
  name: 'posts',
  routes: [
    /*...*/
  ],
})

This also makes it easier to distinguish and navigate individual routes, rather than having 200 calls all in one api export.

Routes definitions

In the code example above, i’ve purposely defined routes inside the a.router definition. Like so:

a.router({ name: 'users', routes: [a.procedure('all', getAllUsers)] }) // ✅ ok

This is due to a fairly common gotcha, caused by typescript treating const x = [] declarations as arrays instead of tuples by default.

const routes = [a.procedure('all', getAllUsers)] // => Call<...>[]
a.router({ name: 'users', routes }) // ❌ ERROR: Breaks type inference - array not a tuple

Since @hulla/api heavily relies on typescript (which is why it has 97.7% smaller bundle size) compared to similar packages, it’s a small price we pay for this luxury — and you’ll have to fix it accordingly:

If you for whatever-reason want to define your routes outside of the router, remember to use the as const assertion.
This tells TypeScript it’s a tuple and not an array - (note this has nothing to do with the package, but with how TS treats declarations)

const routes = [a.procedure('all', getAllUsers)] as const // => [Call<...>] - note the as const
a.router({ name: 'users', routes }) // ✅ now it works