Routers
Organizing procedures into namespaces
Routers organize procedures into namespaces, creating a hierarchical API structure. Routes use dot notation (e.g., users.get, posts.create).
Basic Usage
To organize your procedures into namespaces, you use the router to group related functionality. This creates a hierarchical API structure where routes use dot notation.
// src/api.ts
import { defineContext } from '@deessejs/server'
const { t, createAPI } = defineContext({
context: () => ({ db: myDatabase }),
})
// Define procedures
const getUser = t.query({ ... })
const createUser = t.mutation({ ... })
const getPost = t.query({ ... })
const createPost = t.mutation({ ... })
// Organize into a router
export const appRouter = t.router({
users: t.router({
get: getUser,
create: createUser,
}),
posts: t.router({
get: getPost,
create: createPost,
}),
})Nested Routers
To create deeper hierarchies, you can nest routers within routers. This allows you to organize procedures at multiple levels of specificity.
// src/api.ts
export const appRouter = t.router({
users: t.router({
get: getUser,
create: createUser,
// Nested router for user-related operations
posts: t.router({
getUserPosts: getUserPosts,
createUserPost: createUserPost,
}),
}),
})This creates routes: users.get, users.create, users.posts.getUserPosts, etc.
Route Paths
The route path is determined by the router hierarchy. You can see how the nesting creates the dot-separated route names.
t.router({
users: t.router({
get: getUser,
}),
})
// Route path: users.get
// HTTP endpoint: POST /api/users.getComplete Example
To see how all these concepts work together, here is a complete example that defines procedures, organizes them into routers, and exports the final router.
// src/api.ts
import { defineContext } from '@deessejs/server'
import { z } from 'zod'
import { ok } from '@deessejs/fp'
const { t, createAPI } = defineContext({
context: () => ({ db: myDatabase }),
})
// User procedures
const getUser = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
const user = await ctx.db.users.find(args.id)
return ok(user, { keys: [['users', { id: args.id }]] })
},
})
const createUser = t.mutation({
args: z.object({ name: z.string(), email: z.string().email() }),
handler: async (ctx, args) => {
const user = await ctx.db.users.create(args)
return ok(user, { invalidate: ['users:list'] })
},
})
// Post procedures
const getPost = t.query({
args: z.object({ id: z.number() }),
handler: async (ctx, args) => {
const post = await ctx.db.posts.find(args.id)
return ok(post, { keys: [['posts', { id: args.id }]] })
},
})
export const appRouter = t.router({
users: t.router({
get: getUser,
create: createUser,
}),
posts: t.router({
get: getPost,
}),
})Accessing Routes
When calling from the client, you access routes via dot notation. The client mirrors the router structure you defined on the server.
// Client code
import { client } from './lib/client'
// Call users.get
const user = await client.users.get({ id: 1 })
// Call users.create
await client.users.create({ name: 'Jane', email: 'jane@example.com' })
// Call posts.get
const post = await client.posts.get({ id: 1 })Type Safety
Routers provide full type inference across your entire application. When you define your router, the types flow automatically to your client.
// The client type is inferred from the router
export type AppRouter = typeof appRouter
// client.users.get.query({ id: 1 })
// └─ inferred from router definitionBest Practices
1. Keep Naming Consistent
To maintain clarity, you should use consistent naming conventions across your router. This makes your API predictable and easier to use.
// Do - consistent naming
t.router({
users: t.router({
get: getUser,
create: createUser,
update: updateUser,
delete: deleteUser,
}),
})
// Don't - inconsistent naming
t.router({
users: t.router({
fetch: getUser,
add: createUser,
modify: updateUser,
remove: deleteUser,
}),
})2. Group Related Procedures
To keep your API logical, you should group related procedures together under the same router. This makes your API easier to navigate.
// Do - related procedures grouped
t.router({
users: t.router({
get: getUser,
update: updateUser,
delete: deleteUser,
}),
posts: t.router({
get: getPost,
create: createPost,
}),
})
// Don't - unrelated procedures mixed
t.router({
users: t.router({ get: getUser }),
posts: t.router({ get: getPost }),
// Should be under users or separate category
getEmail: getEmail,
})3. Use Descriptive Names
To make your API self-documenting, you should use descriptive names for your procedures. This helps developers understand what each procedure does without needing comments.
// Do - clear intent
t.router({
users: t.router({
getById: getUser,
getByEmail: getUserByEmail,
}),
})
// Don't - unclear
t.router({
users: t.router({
g1: getUser,
g2: getUserByEmail,
}),
})Anti-Patterns
Don't Create Flat Routers for Complex APIs
When your API grows complex, you should avoid flat routers. A hierarchical structure keeps your API organized and navigable as it scales.
// Don't - flat structure for complex API
export const appRouter = t.router({
getUser: getUser,
createUser: createUser,
updateUser: updateUser,
deleteUser: deleteUser,
getPost: getPost,
createPost: createPost,
// ... becomes hard to navigate
})
// Do - hierarchical structure
export const appRouter = t.router({
users: t.router({
get: getUser,
create: createUser,
update: updateUser,
delete: deleteUser,
}),
posts: t.router({
get: getPost,
create: createPost,
}),
})Don't Use Abbreviations
To keep your API readable, you should avoid abbreviations in router and procedure names. Full words make your API easier to understand.
// Don't - abbreviations
t.router({
usr: t.router({ get: getUser }),
pst: t.router({ get: getPost }),
})
// Do - full words
t.router({
users: t.router({ get: getUser }),
posts: t.router({ get: getPost }),
})Next Steps
- Middleware - Add global hooks
- Server Adapters - Set up HTTP handler
- Client Libraries - Use from client