Blog
API layer in Umami codebase - Part 1.1

API layer in Umami codebase - Part 1.1

Inspired by BulletProof React, I applied its codebase architecture concepts to the Umami codebase.

This article focuses only on the API layer in Umami codebase.

Prerequisite

  1. API layer in Umami codebase — Part 1.0

Approach

The approach we take is simple:

  1. Pick a route, for example, https://cloud.umami.is/analytics/us/websites

  2. Locate this route in Umami codebase.

  3. Review how the data is fetched.

  4. We repeat this process for 3 pages to establish a common pattern, see if there’s any exceptions.

In this part 1.1, we review the websites route and see what library is used to fetch the websites data from the server, where these files are located.

I reviewed the /websites route and found that the following files give us a clear picture about API layer.

  1. WebsitesDataTable.tsx

  2. useUserWebsitesQuery.ts

WebsitesDataTable.tsx

You will find the following code in the WebsitesDataTable.tsx.

import Link from 'next/link';
import { WebsitesTable } from './WebsitesTable';
import { DataGrid } from '@/components/common/DataGrid';
import { useLoginQuery, useNavigation, useUserWebsitesQuery } from '@/components/hooks';

export function WebsitesDataTable({
  userId,
  teamId,
  allowEdit = true,
  allowView = true,
  showActions = true,
}: {
  userId?: string;
  teamId?: string;
  allowEdit?: boolean;
  allowView?: boolean;
  showActions?: boolean;
}) {
  const { user } = useLoginQuery();
  const queryResult = useUserWebsitesQuery({ userId: userId || user?.id, teamId });
  const { renderUrl } = useNavigation();

  const renderLink = (row: any) => (
    <Link href={renderUrl(`/websites/${row.id}`, false)}>{row.name}</Link>
  );

  return (
    <DataGrid query={queryResult} allowSearch allowPaging>
      {({ data }) => (
        <WebsitesTable
          data={data}
          showActions={showActions}
          allowEdit={allowEdit}
          allowView={allowView}
          renderLink={renderLink}
        />
      )}
    </DataGrid>
  );
}

queryResult has the data passed down as a prop to WebsitesTable component to render the data that looks like below:

const queryResult = useUserWebsitesQuery({ userId: userId || user?.id, teamId });

This call fetches the websites. Let’s review the useUserWebsitesQuery.

useUserWebsitesQuery

useUserWebsitesQuery is located at /umami/src/components/hooks/queries/.

and has the following code:

import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery';
import { useModified } from '../useModified';
import { ReactQueryOptions } from '@/lib/types';

export function useUserWebsitesQuery(
  { userId, teamId }: { userId?: string; teamId?: string },
  params?: Record<string, any>,
  options?: ReactQueryOptions,
) {
  const { get } = useApi();
  const { modified } = useModified(`websites`);

  return usePagedQuery({
    queryKey: ['websites', { userId, teamId, modified, ...params }],
    queryFn: pageParams => {
      return get(
        teamId
          ? `/teams/${teamId}/websites`
          : userId
            ? `/users/${userId}/websites`
            : '/me/websites',
        {
          ...pageParams,
          ...params,
        },
      );
    },
    ...options,
  });
}

If you compare this with the below code snippet from Bulletproof react.

import { queryOptions, useQuery } from '@tanstack/react-query';

import { api } from '@/lib/api-client';
import { QueryConfig } from '@/lib/react-query';
import { Discussion, Meta } from '@/types/api';

export const getDiscussions = (
  page = 1,
): Promise<{
  data: Discussion[];
  meta: Meta;
}> => {
  return api.get(`/discussions`, {
    params: {
      page,
    },
  });
};

export const getDiscussionsQueryOptions = ({
  page,
}: { page?: number } = {}) => {
  return queryOptions({
    queryKey: page ? ['discussions', { page }] : ['discussions'],
    queryFn: () => getDiscussions(page),
  });
};

type UseDiscussionsOptions = {
  page?: number;
  queryConfig?: QueryConfig<typeof getDiscussionsQueryOptions>;
};

export const useDiscussions = ({
  queryConfig,
  page,
}: UseDiscussionsOptions) => {
  return useQuery({
    ...getDiscussionsQueryOptions({ page }),
    ...queryConfig,
  });
};

I would say, there is no separate fetcher method defined in Umami hook like getDiscussions method in Bulletproof in react. I would also be interested to learn what useApi hook is defined like:

import { useApi } from '../useApi';

useApi

useApi has the following definition:

import { useCallback } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { getClientAuthToken } from '@/lib/client';
import { SHARE_TOKEN_HEADER } from '@/lib/constants';
import { httpGet, httpPost, httpPut, httpDelete, FetchResponse } from '@/lib/fetch';
import { useApp } from '@/store/app';

const selector = (state: { shareToken: { token?: string } }) => state.shareToken;

async function handleResponse(res: FetchResponse): Promise<any> {
  if (!res.ok) {
    const { message, code, status } = res?.data?.error || {};

    return Promise.reject(Object.assign(new Error(message), { code, status }));
  }
  return Promise.resolve(res.data);
}

export function useApi() {
  const shareToken = useApp(selector);

  const defaultHeaders = {
    authorization: `Bearer ${getClientAuthToken()}`,
    [SHARE_TOKEN_HEADER]: shareToken?.token,
  };
  const basePath = process.env.basePath;

  const getUrl = (url: string) => {
    return url.startsWith('http') ? url : `${basePath || ''}/api${url}`;
  };

  const getHeaders = (headers: any = {}) => {
    return { ...defaultHeaders, ...headers };
  };

  return {
    get: useCallback(
      async (url: string, params: object = {}, headers: object = {}) => {
        return httpGet(getUrl(url), params, getHeaders(headers)).then(handleResponse);
      },
      [httpGet],
    ),

    post: useCallback(
      async (url: string, params: object = {}, headers: object = {}) => {
        return httpPost(getUrl(url), params, getHeaders(headers)).then(handleResponse);
      },
      [httpPost],
    ),

    put: useCallback(
      async (url: string, params: object = {}, headers: object = {}) => {
        return httpPut(getUrl(url), params, getHeaders(headers)).then(handleResponse);
      },
      [httpPut],
    ),

    del: useCallback(
      async (url: string, params: object = {}, headers: object = {}) => {
        return httpDelete(getUrl(url), params, getHeaders(headers)).then(handleResponse);
      },
      [httpDelete],
    ),
    useQuery,
    useMutation,
  };
}

In the Umami, there is no separate function defined for fetcher like in Bulletproof React.

You can learn more about Tanstack React.

Conclusion

We reviewed how Umami websites page has configured Tanstack Query to fetch the list of websites. You will find the hook in hooks/queries/useUserWebsitesQuery.ts. The reusable fetch methods are defined in useApi.ts file.

About me:

Hey, my name is Ramu Narasinga. I study codebase architecture in large open-source projects.

Email: ramu.narasinga@gmail.com

I spent 200+ hours analyzing Supabase, shadcn/ui, LobeChat. Found the patterns that separate AI slop from production code. Stop refactoring AI slop. Start with proven patterns. Check out production-grade projects at thinkthroo.com

References:

  1. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/WebsitesDataTable.tsx

  2. https://github.com/umami-software/umami/blob/master/src/components/hooks/queries/useUserWebsitesQuery.ts#L6

  3. https://github.com/umami-software/umami/blob/master/src/app/(main)/websites/WebsitesTable.tsx#L14

  4. https://github.com/alan2207/bulletproof-react/blob/master/apps/react-vite/src/features/discussions/api/get-discussions.ts