API & Data Hooks

API & Data Hooks

This project uses a combination of a Hono-based API server (@repo/api) and automatically generated React Query hooks (@repo/api-hooks) for efficient and type-safe data fetching and mutations in the frontend applications.

Why You Need This

The API layer provides a single source of truth for your data and the generated hooks give you typed functions for every endpoint. This saves time compared to writing manual fetch calls. See Monorepo Foundations to understand how this package fits into the overall stack.

API Server (@repo/api)

The backend API is built using Hono, a fast and lightweight web framework for Node.js and edge environments. It follows a resource-oriented structure.

Key Features & Technologies

  • Framework: Hono
  • Structure: Routes organized by resource (e.g., users, organizations, tickets) located in packages/api/src/routes.
  • Validation: Uses Zod schemas (often imported from @repo/database/zod) with @hono/zod-validator for robust request validation.
  • Documentation: Leverages hono-openapi to generate an OpenAPI specification directly from the route definitions. This spec is used to generate typed client hooks.
  • Authentication: Integrates with @repo/auth using middleware (authMiddleware, requireAuth) to secure endpoints and provide user context.
  • Type Safety: Strong typing throughout using TypeScript, Hono's generics, and Zod.
  • Error Handling: Consistent error handling patterns using try/catch blocks and HTTPException.
  • API Client Helper: Provides a client.ts helper (used by api-hooks) for potential manual Hono RPC client creation.

Directory Structure (packages/api/src)

src/ ├── index.ts # Main entry point: Hono app setup, global middleware, route mounting ├── middleware.ts # Authentication and authorization middleware ├── client.ts # Helper functions for frontend API client creation (primarily for RPC) ├── types/ # Shared type definitions (e.g., user types) ├── utils/ # General utility functions (e.g., storage helpers) └── routes/ # API routes organized by resource ├── index.ts # Central router registry: imports and mounts all resource routes ├── types.ts # Shared types specific to routes (e.g., AuthContext) ├── util/ # Utilities shared across different routes (e.g., pagination) └── [resource]/ # e.g., users/, organizations/, tickets/ ├── index.ts # Hono router setup for the resource, maps routes to handlers ├── routes.ts # OpenAPI route definitions (`createRoute`) ├── handlers.ts # Business logic implementation └── schema.ts # Zod validation schemas (if needed)

Route Implementation Pattern

  1. Define Schemas (schema.ts, optional): Define Zod schemas for parameters, query, body, or responses if not available from @repo/database/zod.

  2. Define Routes (routes.ts): Use createRoute from @hono/zod-openapi to define metadata (method, path, tags, summary), request schemas (params, query, body), and response schemas for each status code.

  3. Implement Handlers (handlers.ts): Write functions that contain the business logic. These functions should accept a single arguments object containing only the necessary, validated data (e.g., { userId: string, auth: User, body: CreateWidgetInput }) and return the result or throw an HTTPException.

    Warning: Do not pass the full Hono context (c) into handlers. Pass only the required, validated data to keep handlers decoupled and testable.

  4. Map Routes to Handlers (index.ts): In the resource's index.ts, use router.openapi(routeDefinition, handlerWrapper).

    • The handlerWrapper is an async function async (c) => { ... }.
    • Inside the wrapper, perform input validation using c.req.valid('param'), c.req.valid('query'), c.req.valid('json').
    • Extract necessary context (e.g., auth = c.get('user')).
    • Call the corresponding handler function from handlers.ts, passing the validated data and context in the arguments object.
    • Handle potential errors thrown by the handler (catching HTTPException or other errors) and return appropriate c.json(...) responses with status codes.
// Example: packages/api/src/routes/[resource]/index.ts snippet import { OpenAPIHono } from "@hono/zod-openapi"; import { HTTPException } from 'hono/http-exception'; import type { AuthVariables } from "../../middleware"; // Assuming middleware provides auth context import * as handlers from "./handlers"; import { getResourceByIdRoute /* ... other routes */ } from "./routes"; const router = new OpenAPIHono<{ Variables: AuthVariables }>(); router.openapi(getResourceByIdRoute, async (c) => { try { // 1. Validate input const { id } = c.req.valid("param"); // 2. Get context const user = c.get('user'); // 3. Call handler with validated data/context ONLY const result = await handlers.getResourceByIdHandler({ resourceId: id, auth: user }); // 4. Return success response return c.json({ resource: result }, 200 as const); } catch (err) { // 5. Handle errors if (err instanceof HTTPException) { throw err; // Re-throw known HTTP exceptions } console.error(`Error getting resource ${id}:`, err); // Handle specific errors (e.g., not found) if (err instanceof Error && err.message.includes('Not found')) { throw new HTTPException(404, { message: 'Resource not found' }); } // Generic error throw new HTTPException(500, { message: 'Failed to get resource' }); } }); export default router;

API Documentation UI

When the API service is running, the interactive OpenAPI documentation, powered by Scalar, is typically available at the /api/v1/docs endpoint (the exact path depends on the Hono server setup).

Data Hooks (@repo/api-hooks)

This package provides strongly-typed React Query hooks for interacting with the API. It is automatically generated using Orval based on the OpenAPI schema produced by @repo/api.

Key Features & Technologies

  • Generation: Orval reads the /openapi.json endpoint from the running API server (during development or a build step) and generates hooks.
  • Client: Uses Axios (axios-instance.ts) configured with the base API URL.
  • State Management: Built on top of TanStack Query (@tanstack/react-query) for caching, background updates, stale-while-revalidate, mutations, etc.
  • Type Safety: Hooks, parameters, and return types are fully typed based on the OpenAPI schema and generated Zod models (src/models/).

Structure (packages/api-hooks/src)

  • index.ts: Exports all generated hooks and the axiosInstance.
  • axios-instance.ts: Configures the Axios client (e.g., base URL, interceptors for auth tokens).
  • endpoints/: Contains generated service functions, React Query hooks (use[Operation]), and option creators, typically split by API tag.
  • models/: Contains generated Zod schemas and TypeScript types corresponding to API request/response schemas.
  • orval.config.cjs: Configuration file for Orval generation.

Usage

Frontend components import and use the generated hooks directly.

import { useGetUserById, useUpdateUser } from '@repo/api-hooks'; // Import generated hooks import { useQueryClient } from '@tanstack/react-query'; function UserProfile({ userId }: { userId: string }) { const queryClient = useQueryClient(); // Fetching data with useQuery hook const { data: queryResponse, isLoading, error } = useGetUserById({ // Hook parameters are typed based on OpenAPI spec params: { id: userId }, // React Query options can be passed here query: { staleTime: 5 * 60 * 1000, // 5 minutes } }); // Performing mutations with useMutation hook const { mutate: updateUser, isLoading: isUpdating } = useUpdateUser({ mutation: { onSuccess: (updatedUserResponse) => { // Invalidate relevant queries on success queryClient.invalidateQueries({ queryKey: ['users', userId] }); queryClient.invalidateQueries({ queryKey: ['users'] }); // Invalidate list query too console.log('User updated:', updatedUserResponse); }, onError: (err) => { console.error('Update failed:', err); // Display an error message to the user }, } }); if (isLoading) return <div>Loading user...</div>; // Note: Orval hooks often return the full Axios response. Access data via `.data` if (error) return <div>Error loading user: {error.message}</div>; const userData = queryResponse?.data; // Access the actual API response data const handleUpdateName = (newName: string) => { if (!userData?.user) return; updateUser({ params: { id: userId }, // Request body is also typed based on generated models data: { name: newName, email: userData.user.email } // Assuming API expects 'data' property for body }); }; return ( <div> <h1>{userData?.user.name}</h1> {/* ... UI to update name and call handleUpdateName ... */} {isUpdating && <span>Updating...</span>} </div> ); }

Note: Check the generated hook signatures in @repo/api-hooks/src/endpoints/ to see the exact structure expected for parameters (params, data for body) and how to access the response data (often via response.data).

Generation Workflow

  1. Make changes to the @repo/api routes (add/modify endpoints, update schemas in routes.ts).
  2. Ensure the API development server is running (so Orval can fetch /openapi.json).
  3. Run pnpm --filter @repo/api-hooks generate (or the equivalent command configured in your root package.json, often just pnpm generate:hooks) to regenerate the hooks in @repo/api-hooks.
  4. Update frontend components to use the new/modified hooks.

This setup ensures that the frontend stays synchronized with the backend API, providing end-to-end type safety and reducing runtime errors.