Skip to content

API service modules

Domain services live under apps/web/src/lib/services/. Each module wraps a slice of the Workers API using request(). They are the only place pages and query hooks should go for HTTP.

Related: API core · Query · Permissions.


Responsibilities

A service module does:

  • Map a method name → HTTP method + path
  • Map params / body to Axios config
  • Return a stable frontend type (often unwrapped nested fields, e.g. res.user)
  • Stay free of React, routing, and permission UI decisions

A service module does not:

  • Own loading / error UI state (that is Query + page)
  • Decide whether a button is visible (that is permissions)
  • Cache server data (that is TanStack Query)

Module map

Exported from apps/web/src/lib/services/index.ts:

ServiceTypical routesNotes
authService/auth/*Login, register, logout, verify
meService/me/*Profile, settings, security surfaces
sessionService/me/session*Session list / revoke
avatarServiceavatar upload / getMultipart where needed
usersService/users*List, create, detail, permissions, activities
departmentsService/departments*Org departments
maintenanceService/maintenance*Lifecycle CRUD + workflow actions
systemService/system*Permissions catalog, monitor, logs, audit, settings
notificationsServicenotificationsInbox / unread
dashboardServicedashboard analyticsRole dashboards (when used)

Add a new folder under lib/services/<domain>/ when a new API domain appears — do not dump unrelated methods into users or system.


Conventions

typescript
export const usersService = {
  async getUsers(params?: UsersListParams, options?: { handleErrors?: boolean }) {
    return request<UsersListResult>({
      method: 'GET',
      url: '/users',
      params: /* stringified query */,
      ...options,
    });
  },

  async createUser(payload: CreateUserPayload, options?: { handleErrors?: boolean }) {
    const res = await request<{ user: User }>({
      method: 'POST',
      url: '/users/new',
      data: payload,
      ...options,
    });
    return res.user; // unwrap nested envelope fields for callers
  },
};
  • Prefer named options { handleErrors?, context? } on every public method.
  • Keep endpoint-specific types next to the service (or in apps/web/src/types/).
  • Document permission requirements in JSDoc when the Worker enforces them (user.create, etc.).

How this connects to Query

  1. Service = transport
  2. src/query/keys.ts + domain hooks = cache keys + invalidation
  3. Page = UI wiring

Example: useUsersListQuery calls usersService.getUsers; mutations invalidate queryKeys.users.lists() / detail(id). See Query.


Checklist for a new endpoint

  1. Confirm Worker route + Zod schema exist
  2. Add / extend the domain service method
  3. Add query key + hook if the page needs cached server state
  4. Gate UI with permissions; never rely on hiding the call alone