DeesseJS RPC

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.get

Complete 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 definition

Best 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,
  }),
})

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

On this page