Skip to content

Instantly share code, notes, and snippets.

@dadamssg
Last active August 14, 2025 17:20
Show Gist options
  • Save dadamssg/abe7f307459121664df82d83986cf35c to your computer and use it in GitHub Desktop.
Save dadamssg/abe7f307459121664df82d83986cf35c to your computer and use it in GitHub Desktop.

Interacting with the API

fetchTyped

The application is using getLoadContext which gets the user and creates an api client bound to the current user. This api client, fetchTyped, wraps the native fetch function and will automatically include the jwt token stored in the session for that request. It also accepts a zod schema to validate the returned json. This is located on the app context object that can be retrieved in actions and loaders.

export async function loader({ context }: Route.LoaderArgs) {
  const ctx = getAppContext(context)

  const schema = z.object({
    person: z.object({
      id: z.string(),
      firstname: z.string(),
      // ...
    }),
  })

  const res = await ctx.fetchTyped(`/contacts/123`, schema)
  const firstname = res.person.firstname

  // ...
}

The third argument to fetchTyped is a RequestInit object from native fetch.

const data = await ctx.fetchTyped(`/contacts/123`, schema, {
  method: 'POST',
  body: JSON.stringify(payload),
})

The fetchTyped client will also emit logs that can be seen from the browser console to aid in debugging.

Organization

All api interactions should be located in app/module/<module-name>/<resouce-name>-client.server.tsx. The resource name should be singular. Ex. contact-client.server.tsx, not contacts-client.server.tsx.

These modules should export functions to interact with the api. Example:

// app/module/note/note-client.server.tsx

type UpdateNotePayload = {
  contents: string
}

export async function updateNote(ctx: AppContext, { noteId, payload }: { noteId: string; payload: UpdateNotePayload }) {
  const schema = z
    .object({
      note: z.object({
        id: z.string(),
      }),
    })
    .or(errorSchema)

  return ctx.fetchTyped(`/notes/${noteId}`, schema, {
    method: 'POST',
    body: JSON.stringify({ note: payload }),
  })
}

When importing, prefer to import all functions in the module instead of individual functions:

// app/routes/app.contact.$id.view/route.tsx
import * as contactClient from '~/app/module/contact/contact-client.server'

// usage
contactClient.createContact(ctx, config)

Rules for api functions

The function names should not allude to an api being used.

Good:

  • getNote
  • getNotes
  • createNote
  • updateNote
  • deleteNote

Bad:

  • fetchNote

These functions should only contain at most two arguments: ctx, and a configuration object. Descriptive id field names should be used to decrease ambiguity inside the configuration object. Ex. contactId, not id.

The functions should be ui agnostic. For example, they should not return toast messages. This should be done in the route or component files. Example:

// app/routes/app.contact.$id.view/route.tsx

export async function action({ request, params, context }: Route.ActionArgs) {
  const formData = await request.formData()
  const ctx = getAppContext(context)
  const result = await updateContact(ctx, {
    contactId: params.id,
    payload: {
      firstname: String(formData.get('firstname') ?? ''),
      // ...
    },
  })
  // use react router's data() utility to be able to send proper status code
  return data(result, { status: result.success ? 200 : 400 })
}

// clientAction should be origin of toasts, not useEffects
export async function clientAction({ serverAction, request }: Route.ClientActionArgs) {
  const result = await serverAction()
  toast({
    title: result.toast.title,
    variant: result.toast.variant,
  })
  return result
}

// Wrapper function to put together toast messages based on the response.
// This code is specific to this use case of the api function.
async function updateContact(...args: Parameters<typeof contactClient.updateContact>) {
  const result = await contactClient.updateContact(...args)
  if ('person' in result) {
    return {
      success: true,
      person: result.person,
      toast: {
        title: 'Contact updated!',
      },
    }
  }
  return {
    success: false,
    person: null,
    toast: {
      title: result.errors[0]?.detail ?? 'Error',
      variant: 'destructive',
    },
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment