Tuyau: Typesafe API Toolkit for AdonisJS

repository·main·Indexed 42 days ago

https://github.com/julien-r44/tuyau

A toolkit for building end-to-end typesafe APIs within the AdonisJS ecosystem. It generates a typesafe client for the frontend to consume backend routes, featuring specialized integrations for Tanstack Query via @tuyau/react-query and complex type serialization via @tuyau/superjson.

Tokens
17K
Snippets
53
Records
75
Agent score
52%

What's inside Tuyau

  1. Overview of Tuyau tools

    main

    Tuyau is a set of tools designed to create typesafe APIs using AdonisJS. It provides a way to bridge the gap between your AdonisJS backend and your frontend by generating an E2E typesafe client.

    The monorepo consists of several specialized packages:

    • @tuyau/core: The primary package containing the backend integration for AdonisJS and the E2E typesafe client used by the frontend to consume APIs.
    • @tuyau/react-query: Provides integration for Tanstack Query, allowing you to use Tuyau clients within React Query hooks.
    • @tuyau/superjson: Enables seamless integration with SuperJSON to handle complex data types (like Dates, Maps, Sets) across the API boundary.

    For detailed implementation guides, refer to the official documentation at https://tuyau.julr.dev.

  2. Use @tuyau/superjson for complex type serialization

    main
    @tuyau/superjson is a plugin designed for Tuyau and AdonisJS. It enables seamless serialization and deserialization of complex JavaScript types—such as Date, Map, and Set—which are typically lost or converted to strings during standard JSON operations.
  3. Understand Tuyau query and mutation key structures

    main

    Tuyau uses structured arrays as keys for TanStack Query integration to allow precise cache management.

    • TuyauQueryKey: Used for queries. It is a tuple where the first element is an array of route path segments (readonly string[]). The second element is an optional metadata object containing request (of type RawRequestArgs) and type (which can be 'infinite' or 'query', but not 'any').
    • TuyauMutationKey: Used for mutations. It is a simpler tuple containing only the route path segments (readonly string[]).
    export type TuyauQueryKey = [
      readonly string[],
      { request?: RawRequestArgs<any>; type?: Exclude<QueryType, 'any'> }?,
    ]
    
    export type TuyauMutationKey = [readonly string[]]
  4. Transform schema endpoints into React Query decorated nodes

    main

    The TuyauReactQuery<R> type transforms a raw schema registry (where R is a record of SchemaEndpoints) into a tree of decorated endpoints. Each endpoint in the resulting tree is enhanced with React Query functionality based on its HTTP method:

    • Queries (GET/HEAD): Nodes are decorated with query functions (DecorateQueryFn) and infinite query functions (DecorateInfiniteQueryFn).
    • Mutations (POST/PUT/DELETE/etc.): Nodes are decorated with mutation functions (DecorateMutationFn).
    • Routing: All nodes include DecorateRouterKeyable to support router-based keying.

    The transformation recursively traverses the schema tree, handling nodes that are both endpoints and parents of other nodes. It automatically omits internal endpoint metadata (methods, pattern, types) from the decorated structure to provide a clean API.

    // Example usage of the resulting type structure
    type MyReactQuery = TuyauReactQuery<MySchemaRegistry>;
    
    // A node in MyReactQuery might look like:
    // myEndpoint.query({ ...options }) // if GET
    // myEndpoint.mutate({ ...options }) // if POST
    // myEndpoint.subPath.query({ ...options }) // if nested
  5. Understand TuyauMutationOptions types

    main

    Tuyau provides specialized types to manage the lifecycle of mutation options between user input and the final React Query configuration:

    • TuyauMutationOptionsIn: The type for options provided by the developer. It extends TanStack's UseMutationOptions but omits mutationKey and mutationFn because Tuyau manages these automatically.
    • TuyauMutationOptionsOut: The fully resolved type. This includes the auto-generated mutationKey and mutationFn, making it compatible with TanStack's useMutation hook.
    • TuyauReactMutationOptions: A callable interface found on decorated mutation endpoints. When called with user options, it returns the fully resolved TuyauMutationOptionsOut.
  6. How route name and path pattern matching works

    main

    The TuyauRouter uses two different matching strategies depending on whether you are matching route names or URL paths:

    Route Name Pattern Matching

    When using has() or current(routeName), the router matches the route name (e.g., users.show). It supports the * wildcard, which matches one or more dot-separated segments via recursive backtracking.

    • users.* matches users.show, users.edit, or users.profile.settings.

    URL Path Pattern Matching

    When resolving the current URL against the registry, the router matches the URL pathname against the AdonisEndpoint.pattern. It supports:

    • :param: A named dynamic segment (e.g., /users/:id matches /users/123).
    • :param?: An optional named dynamic segment.
    • *: A wildcard that matches all remaining segments of the path (e.g., /files/* matches /files/images/logo.png).
  7. Understand the TuyauVueQuery client type structure

    main

    The TuyauVueQuery type is the top-level type for a Tuyau Vue Query client. It is generated by recursively transforming a route tree (defined by SchemaEndpoint objects) into a client that provides type-safe decorators.

    Key characteristics of the transformed client:

    • Endpoint Nodes: Each endpoint is resolved into an EndpointNode.
      • GET or HEAD endpoints provide query and infinite query methods.
      • Other methods (e.g., POST, PUT) provide mutation methods.
    • Router Keyability: Every level of the route tree (both endpoints and nested route segments) includes methods from DecorateRouterKeyable for path-level key and filter access.
    • Recursive Transformation: The client maintains the structure of your route tree, allowing you to traverse segments to reach specific endpoints.
  8. Understand `TuyauMutationOptionsIn` and `TuyauMutationOptionsOut`

    main

    When working with mutations in @tuyau/vue-query, there are two distinct stages of options:

    1. TuyauMutationOptionsIn: This is the interface for options you provide as a developer. It extends TanStack's MutationObserverOptions but omits mutationKey and mutationFn because these are automatically managed by Tuyau based on the route.

    2. TuyauMutationOptionsOut: This is the interface for the fully resolved options object. It includes the auto-generated mutationKey (of type TuyauMutationKey) and the mutationFn required to execute the request via the Tuyau client.

  9. Understand the Tuyau Registry structure

    main

    The TuyauRegistry is the central source of truth for your API. It consists of a routes object mapping endpoint names to AdonisEndpoint definitions. It can optionally include a $tree property, which represents a hierarchical ApiDefinition used to generate a tree-structured API client via TransformApiDefinition.

    export interface TuyauRegistry<,
      Routes extends Record<string, AdonisEndpoint> = Record<string, AdonisEndpoint>,
      Tree = unknown,
    > {
      routes: Routes
      $tree?: Tree
    }
  10. Handle file uploads and multipart/form-data

    main

    The Tuyau client automatically detects if a request body contains a File, FileList, or Blob (on the server). If a file is detected, the client automatically switches the request content type to multipart/form-data and serializes the body using object-to-formdata with indices enabled.

    // The client detects the File and uses multipart/form-data automatically
    await client.post('/upload', {
      body: {
        avatar: myFileInstance,
        description: 'Profile picture'
      }
    })
  11. Set up Tuyau with React Query using createTuyauContext

    main

    To integrate Tuyau with React Query in a React application, use createTuyauContext. This function generates a set of type-safe provider and hook components that manage both the raw Tuyau client and the React Query-enhanced utilities.

    Steps to integrate:

    1. Call createTuyauContext<TRegistry>() to initialize your context (passing your registry type for full type safety).
    2. Wrap your application (or a specific subtree) with the returned TuyauProvider.
    3. Pass the required queryClient (from @tanstack/react-query) and the client (the Tuyau instance) to the provider.
    4. Use the returned useTuyau hook to access React Query-powered utilities, or useTuyauClient to access the raw Tuyau client.
    import { createTuyauContext } from '@tuyau/react-query';
    import { QueryClient } from '@tanstack/react-query';
    
    // 1. Initialize context with your Registry type
    type MyRegistry = ...; 
    const { TuyauProvider, useTuyau, useTuyauClient } = createTuyauContext<MyRegistry>();
    
    const queryClient = new QueryClient();
    const client = ...; // Your Tuyau client instance
    
    function App() {
      return (
        // 2. Wrap with TuyauProvider
        <TuyauProvider queryClient={queryClient} client={client}>
          <MyComponent />
        </TuyauProvider>
      );
    }
    
    function MyComponent() {
      // 3. Use hooks inside the provider
      const tuyauQuery = useTuyau();
      const client = useTuyauClient();
      
      return <div>...</div>;
    }