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:
Argument | Type | Status | Description |
---|---|---|---|
name | string | Required | Used for request deduping, key encoding |
routes | readonly [ Procedures , or other custom methods] | Required | Routes definitions, calls you’ll be executing |
interceptors | Interceptors | Optional | Utility functions ran on every matching event |
adapters | Adapters | Optional | Integrations 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
]
})
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'
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()
call()
- the main way to interact, the main way tocall
your procedures (or other route methods).routerName
- defined in router definitionname: string
routeNames
- array of passed route namesRoutes<RouteName extends string>[number]
context
- the custom context passed to router....AdapterMap
- any potentialadapters
/integrations
passed.methods
- array of passed route methods (if only procedures['call']
). Useful if you have multiple inegrations likecustom methods
Best practices
Router doesn’t really have any gotchas. These are more of general suggestions of tested by time best practices.
Project structure
- Keep your routers minimal
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