Overcoming Challenges in Building an API with RTK Query

Continuing on from where I had left off yesterday, I added the 15 endpoints to my apiSlice using RTK Query to point to the REST API managing icons via PHP/MySQL.

  • GET, PUT /icon/sets/{setId}
  • GET /icon/sets/{setId}/icons
  • GET, PUT /icon/sets/{setId}/icons/{iconId}
  • GET, POST /icon/sets/{setId}/tags
  • GET, PUT, DELETE /icon/sets/{setId}/tags/{tagId}
  • GET, POST /icon/sets/{setId}/filters
  • GET, PUT, DELETE /icon/sets/{setId}/filters/{filterId}

The growing size of the apiSlice file is starting to impact its manageability. I’d like to revisit my past modularization approach and explore the possibility of creating separate API slices for each REST document. This would not only enhance organization but also open the door for code splitting, ensuring that unnecessary logic isn’t downloaded by clients without the appropriate permissions.

As a general guideline, I prefer to keep code files below 300 lines to enhance readability and maintainability. However, there are exceptions, such as data-driven libraries like my emoji library. Although these files can be quite large, I prioritize organization through techniques like alphabetical sorting and leverage code generation to streamline the development process.

Google Gemini: Imagen 3

create an image to feature with the article i posted

Given the current best practices, I’m considering OpenAPI as a potential solution for managing a single slice per server. However, I’m cautious about the implications for caching and tag invalidation. To prioritize immediate functionality, I’ll focus on implementing icon management and then explore OpenAPI integration at a later stage.

Let’s re-evaluate what Redux Toolkit has to say about API Slices.

While code splitting seems promising, I’ve faced difficulties with type context preservation during modularization. To maintain type safety, I’ve had to define additional types.

While the injection is successful, I’ve identified a potential circular dependency between the apiSlice and icon endpoints. To resolve this, I’ll need to separate the tags.

state/api/endpoints/icon.ts
import { apiSlice } from "../apiSlice";
import { tagIconSetFilters, tagIconSetIcons } from "../tags";

apiSlice.injectEndpoints({
  endpoints: (build) => ({
    deleteIconSetFilter: build.mutation<
    void,
    {
      setId: string,
      id: string
    }
  >({
    query: ({ setId, id }) => ({
      url: `/icon/sets/${setId}/filters/${id}`,
      method: 'DELETE'
    }),
    invalidatesTags: (_result, _error, { setId, id }, _meta) => [
      tagIconSetFilters(`${setId}-${id}`),
      tagIconSetIcons(`${setId}-list`)
    ]
  })
  })
})

This approach is effective and avoids the need for immediate code splitting. It maintains separation of concerns and type safety. If required, I can enhance modularization by injecting endpoints for individual stores while leveraging a shared slice and caching. Moreover, I can create types specific to each endpoint file without polluting the apiSlice.

Here is the shared file for cache tags

state/api/tags.ts
import emoji from '@lewismoten/emoji';

const TAG_TYPE = `type ${emoji.fox}` as const;
const TAG_TYPE_FEATURE =
  `${TAG_TYPE} feature ${emoji.fairy}` as const;
const TAG_ICON = `icon ${emoji.framedPicture}` as const;
const TAG_ICON_SETS = `${TAG_ICON} sets` as const;
const TAG_ICON_SET_ICONS = `${TAG_ICON} icons` as const;
const TAG_ICON_SET_TAGS = `${TAG_ICON} tags` as const;
const TAG_ICON_SET_FILTERS = `${TAG_ICON} filters` as const;

export const tagTypes = [
  TAG_TYPE,
  TAG_TYPE_FEATURE,
  TAG_ICON_SETS,
  TAG_ICON_SET_FILTERS,
  TAG_ICON_SET_ICONS,
  TAG_ICON_SET_TAGS
];

type tagIdentifier = string | { id: string };

interface Tag<T extends string = string> {
  (value: tagIdentifier): { id: string, type: T }
}

export const tagType: Tag<typeof TAG_TYPE> =
  (value) => tagBase(value, TAG_TYPE);

export const tagTypeFeature: Tag<typeof TAG_TYPE_FEATURE> =
  (value) => tagBase(value, TAG_TYPE_FEATURE);

export const tagIconSets: Tag<typeof TAG_ICON_SETS> =
  (value) => tagBase(value, TAG_ICON_SETS);

export const tagIconSetIcons: Tag<typeof TAG_ICON_SET_ICONS> =
  (value) => tagBase(value, TAG_ICON_SET_ICONS);

export const tagIconSetTags: Tag<typeof TAG_ICON_SET_TAGS> =
  (value) => tagBase(value, TAG_ICON_SET_TAGS);

export const tagIconSetFilters: Tag<typeof TAG_ICON_SET_FILTERS> =
  (value) => tagBase(value, TAG_ICON_SET_FILTERS);

const tagBase = <Type extends string>(value: tagIdentifier, type: Type) => {
  switch (typeof value) {
    case 'string':
      return { id: value, type };
    case 'object': return ({ id: value.id, type })
  }
}

I’m recognizing a potential scalability issue with the current structure. Although I can relocate the tag builder functions to their corresponding endpoint files, the tagTypes must be exposed to the API. This necessitates keeping the constants in the main file and exporting them for use in the endpoint files. Despite my efforts, it appears difficult to fully separate the apiSlice from dependencies tied to the icon endpoints. The caching mechanism is a significant obstacle in achieving complete decoupling.

tag(id: `icon/sets/${setId}/icons/${iconId}`, type: 'api')

This approach allows the API’s caching to be agnostic, meaning it doesn’t require knowledge of the specific icon endpoints. Despite this, it can still effectively work with tags that are provided or invalidated. However, I’m questioning the need for the ‘type’ attribute for a tag. What additional benefits does it offer beyond the identifier? For example, with a data list, we can specify both icon/sets and /icon/sets/${setId} for each value, enabling us to invalidate the entire list using ‘icon/sets’ or individually using /icon/sets/1. I suspect there might be a missing element. Perhaps the ‘type’ attribute is primarily intended to assist beginners in understanding the caching concepts in RTK Query.

Studio 330 Makerspace & Memory Lab

We’re thrilled to announce the grand opening of our Makerspace & Memory Lab! We celebrated with a ribbon-cutting ceremony attended by local officials, media outlets, and the chamber of commerce. As a library trustee, I participated in an emergency board meeting following the event. We voted to request county approval for replacing the malfunctioning parking lights. Although we’re responsible for maintenance, the county has historically covered it, but has abruptly stopped without notice. Our contract requires county approval for property improvements. The lights have been repeatedly replaced over the years due to frequent burnouts. We plan to replace them with LED fixtures to address the ongoing issue. The local media covered both events, and I’ll share links to any coverage soon.

RTK Query Cache

One advantage of using types is that specifying only the type will clear the entire cache associated with that type. However, I can define tags with an ID of /icon/ for all endpoint requests to my icon API, along with tags for specific content. This enables me to invalidate all requests to any icon API endpoint by targeting that specific ID.

I’ve discovered a way to bypass the tag type dependency, allowing the initial API to remain unaware of the tags for the icon endpoints. Before injecting the endpoints, you need to enhance the apiSlice and utilize its return value for endpoint injection.

apiSlice.enhanceEndpoints({
  addTagTypes: [
    TAG_ICON_SETS,
    TAG_ICON_SET_FILTERS,
    TAG_ICON_SET_TAGS,
    TAG_ICON_SET_ICONS
  ]
}).injectEndpoints({
  endpoints: (build) => ({
    getIconSet: build.query<
      {
        id: string,
        label: string,
        value: string
      },
      string
    >({
      query: (id) => `/icon/sets/${id}`,
      providesTags: (_result, _error, id) => 
          [{id, type: TAG_ICON_SETS}]
    }),

This is a significant improvement. I believe this approach aligns more closely with the intended use of the Toolkit while maintaining type safety. However, even with these changes, the image endpoints alone exceed 300 lines of code. By utilizing types to represent input and output parameters, I was able to reduce the code size by 30%.

Before I move onto the

The frequent color changes between white and black are becoming irritating. Thankfully, the site has transitioned to a dark theme. Unfortunately, the code is difficult to read in the editing area. It seems to be a light gray color on a white background.

Screenshot from an early iteration of this article with a code block of ghost white text on a white background

Returning to the previous topic, I’d like to discuss the use of slugs. Previously, I was using numeric IDs to access resources. I’ve updated the frontend to support strings, and the backend now sends all numeric IDs as strings. This change paves the way for the current approach. Currently, tags and filters are using their labels as IDs. While this works, it can lead to issues with matching paths if forward slash characters are used. Additionally, I’m passing an array of these labels to the search page for filtering. To address these concerns, I’ll sanitize the tag labels to ensure compatibility with both routing paths and comma-delimited query string values. Many content management systems (CMS) employ slugs to access resources, improving URL readability and SEO. I’ll follow this approach by sanitizing the labels for use in folder names and delimited list values, providing these sanitized versions as identifiers to the end-user.

url_safe_string.php
<?php
function url_safe_string($string)
{
    // Convert unicode to compatible base charaters + accents
    // ie - letter "e" followed by an acute accent mark
    $string = Normalizer::normalize($string, Normalizer::FORM_C);

    // PascalCase and CamelCase
    $string = preg_replace('/([a-z])([A-Z])/u', '$1-$2', $string);

    // SnakeCase
    $string = preg_replace('/_+/', '-', $string);

    $string = strtolower($string);

    // Group successive digits
    $string = preg_replace('/(\d{2,})/', '-\\1-', $string);

    // Replace spaces with dashes
    $string = str_replace(' ', '-', $string);

    // Only allow a-z, digits, and dash
    $string = preg_replace('/[^a-z0-9-]/', '', $string);

    // Remove repeated dashes
    $string = preg_replace('/-+/', '-', $string);

    // Remove leading/trailing dashes
    $string = trim($string, '-');

    return $string;
}

As expected, my icons are in PascalCase, which I considered during the naming process. To improve compatibility with other languages, I normalized the Unicode characters, converting them into base characters followed by their accents (e.g., ‘e’ followed by an acute accent mark).

This opens up an interesting possibility in our database. I generally use Unicode character sets throughout, which require four bytes per character. The slug, on the other hand, is restricted to lowercase letters, digits, and dashes. As these characters are all Latin characters, they can be stored in a single byte each.

slug VARCHAR(64) NOT NULL CHARACTER SET latin1 COLLATE latin1_general_ci,

An additional advantage of using slugs is that they allow us to conceal the true identity from the end-user, providing them with the flexibility to modify what they perceive as the identity. However, this comes at the cost of increased storage requirements and the need for a unique index in every table that utilizes slugs.

I’ve just realized that I need to implement the URL-safe string function not only in PHP but also in JavaScript for generating the seed scripts to populate the database.

The SQL scripts are now being generated with slugs. It seems I overlooked a few details in the PHP version, as I couldn’t test it without making necessary changes to the database. Here’s the JavaScript version of the function for sanitizing strings to be used as identity slugs.

const urlSafeString = value => value
  .normalize('NFD')
  .replace(/[\u0300-\u036f]/g, '')
  .replace(/([A-Z])/g, '-$1')
  .replace(/_+/, '-')
  .toLowerCase()
  .replace(/\d{2,}/g, '-$&')
  .replace(/\s/g, '-')
  .replace(/[\\\/\|\+\.\?\[\]={}:;"'<>,.\(\)\*&\^%\$#@!~`]/g, '-')
  .replace(/[^a-z0-9-]/g, '')
  .replace(/-+/, '-')
  .replace(/^-*(.*?)-*$/, '$1');

I switched to NFD normalization instead of NFC and removed combining marks. Additionally, I converted symbols into delimiters before removing invalid characters.

The white-out effect on the code while editing posts in the “Message” theme by Automatic is quite annoying. I’ve filed a bug report at @Automatic/themes#8239 to bring this issue to their attention. Considering the recent departure of Automatic staff due to the Trademark vs Extortion lawsuit, I doubt theme development will be a priority anytime soon. Let’s see if switching to a different template resolves the problem.

The ‘Colorloops’ theme seems to exhibit the same issue. While there’s slightly more contrast, it’s possible that dark backgrounds aren’t typically tested with code blocks.

Editing Codeblocks with Colorloops theme

Many of the available themes are quite poor quality. They require significant customization to make my site look presentable. Some themes even lack basic elements like menus or links to the blog page. While the ‘Videomaker’ theme is suitable for editing code, I’ll need to revisit and modify all of the templates.

I think I’m done. This has been quite time-consuming. WordPress should consider adding a feature that enables users to build custom themes using the existing editing blocks. I’m essentially removing all the content from a free theme to create my own configuration, just to maintain the color scheme they’ve used.

I’ve incorporated slug fields into the database creation scripts and configured the SQL generation to display the icon tables with unique and sanitized slugs. Now, I’ll rebuild the database to verify the accuracy of my implementation.

I’ve identified some issues with my SQL creation scripts. In the past, I’ve developed a linter to verify the accuracy of database field names, index names, foreign keys, and other aspects of my database schema. While not perfect, it has helped me catch potential errors that I might have overlooked.

I encountered a charset error, indicating that the charset was not specified. However, I had already defined it. It seems the system was expecting utf8mb4 for all varchar, char, and text fields. To resolve this, I’ll need to modify the code to use a regular expression to parse the charset and collation. latin1 should only be used for slugs at this time.

if (
  dataType.startsWith('VARCHAR')
  || dataType.startsWith('CHAR')
  || dataType === 'TEXT') {
  const expectedCharSet = 'CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
  if (rest === undefined || !rest.startsWith('CHARACTER SET')) {
    console.warn(`Charset not specified for ${tableName}.${name}`);
  } else if (rest !== expectedCharSet) {
    console.warn(`Incorrect characterset specified for ${tableName}.${name}`);
    console.log(`Got : ${rest}`);
    console.log(`Want: ${expectedCharSet}`);
  }
}

Here is an update.

const latinFields = ['slug'];
if (
  dataType.startsWith('VARCHAR')
  || dataType.startsWith('CHAR')
  || dataType === 'TEXT') {
  const charsetPattern = /CHARACTER SET ([^\s]+) COLLATE ([^\s]+)/
  if (rest === undefined || !charsetPattern.test(rest)) {
    console.warn(`Charset not specified for ${tableName}.${name}`);
  } else {
    const [, charset, collate] = rest.match(charsetPattern);
    if (!collate.startsWith(charset)) {
      console.warn(`Character set does not match collate for ${tableName}.${name}`);
      console.log(`Character set : ${charset}`);
      console.log(`Collate: ${collate}`);
    } else if (charset === 'latin1' && collate !== 'latin1_general_ci') {
      console.warn(`Wrong collation for ${tableName}.${name}`);
      console.log(`Expected: latin1_general_ci`);
      console.log(`Collate: ${collate}`);
    } else if (charset === 'utf8mb4' && collate !== 'utf8mb4_unicode_ci') {
      console.warn(`Wrong collation for ${tableName}.${name}`);
      console.log(`Expected: utf8mb4_unicode_ci`);
      console.log(`Collate: ${collate}`);
    } else if (charset === 'utf8mb4' && latinFields.includes(name)) {
      console.warn(`Wrong character set for ${tableName}.${name}`);
      console.log(`Expected: latin1`);
      console.log(`Got: ${charset}`);
    } else if (charset === 'latin1' && !latinFields.includes(name)) {
      console.warn(`Wrong character set for ${tableName}.${name}`);
      console.log(`Expected: utf8mb4`);
      console.log(`Got: ${charset}`);
    }
  }
}

The issue now lies with the index in IconTags. My mistake, this is a many-to-many relationship table. Since it’s never directly requested, a slug isn’t necessary. I was confusing it with IconSetTags.

I’m currently encountering difficulties when attempting to drop tables.

-- Drop Tables Batch 1
DROP TABLE IF EXISTS IconSetFilters, IconTags, 
ItemDateDetails, ItemFieldOrders, ItemHidden,
ItemIdentifierParts, ItemImports, ItemIntegers, 
ItemListValues, ItemLongTexts, ItemObjects, 
ItemShortTexts, ItemSpatials, ItemTypes, 
ListValueAlternatives, ListValueApprovals, 
ListValueDescriptions, ListValueImports, 
ListValueOrders, OfficialListValues, 
RateLimitsByIP, Updates
;;

Hmm… it’s not a foreign key constraint error. Its talking about a syntax error. I’ve got multiple queries enabled. I’m splitting the file by “;” on it’s own line alone. Let’s try splitting with the double delimiters as well.

const getQueries = () => fs.readFileSync(
  'dist/db/rebuild.sql',
  'utf8'
)
  .split(/\r\n;\r\n|;;/g)
  .filter(query => query.trim() !== '');

That did the trick. Now I’m running into trouble with the latin character sets.

The large language models seemed unable to accurately identify the issue with my table. They simply complained about my use of Latin instead of UTF, suggesting potential Unicode compatibility problems. However, this is intentional.

It’s noteworthy that the NOT NULL constraint should be placed after the character set specification.

slug VARCHAR(64) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL UNIQUE

I’ve updated the remaining tables. The database is now deployed with slugs for icons, tags, and filters. The REST API routes have been established, and the necessary logic is in place for each route. The apiSlice that integrates with RTK Query has a separate file that both enhances endpoints with icon tags and injects endpoints for communicating with the icon endpoints. Let’s proceed with connecting the icon picker dialog to request icons from the API and pass the data to the infinite scroll.

Before continuing, I moved the remaining endpoints out of the apiSlice. The apiSlice is now condensed to 81 lines of code, concentrating on the base query and the mechanism for using the JWT refresh token to acquire a new authorization token upon expiration.

JWT Vulnerabilities

Regarding JWT, I discovered that many organizations relied on various libraries to validate tokens but neglected to verify the algorithm. This oversight led to vulnerabilities. Users discovered that setting the JWT “alg” to “none” allowed them to enter arbitrary data in the payload/claims without requiring a signature. This is known as the “JWT None Attack.” As I’ve implemented a basic implementation, I’m not susceptible to this type of attack. Additionally, I’ve hardcoded the expected value for the “alg” field. Several companies were affected by similar vulnerabilities, including Auth0 (2017), Red Hat (2017), and Firebase (2018). While there seems to be a range of vulnerabilities, many of the reported incidents occurred between 2015 and 2020. I believe security practices have evolved to address these issues and prevent future exploits.

When considering JWT security, it’s important to be aware of various attack vectors. These include signature verification bypass, weak secret keys, missing or incorrect algorithms, token hijacking, replay attacks, JWT injection, and the exposure of sensitive data. I’ve taken steps to mitigate some of these risks, such as using a cryptographically strong secret key, implementing rate limiting, and avoiding the use of symmetric algorithms for third-party verification. However, it’s crucial to remain vigilant and continue to explore additional security measures to protect against evolving threats.

Endpoints

Returning to the code, I loaded the site and encountered some issues after separating the endpoints. Instead of importing the apiSlice to access my endpoints, I now needed to import the specific group of endpoints associated with the “feature.”

I’m currently reviewing the IconPicker component and deleting over a thousand lines of code. A significant portion is redundant data, while the rest involves complex parsing and categorization logic. After the initial pass, I’ve reduced the code from 1,732 lines to 246. It’s a relief to eliminate unnecessary code. Now, I’ll focus on connecting the API calls to retrieve tags, filters, and icons.

Everything has been converted. However, when the icon picker loads, it’s completely blank. I’m encountering an error while requesting data about the icon set itself. It’s a CORS error. The request for GET /api/icon/sets/mui-icons-material should map to api/icon/set-get.php?id=mui-icons-material. Upon further inspection, I realized that I haven’t updated any of the PHP scripts to use slugs. In fact, I don’t believe I’ve deployed any of the PHP scripts to the server. Reviewing the PHP scripts, I noticed that they follow an older database schema that doesn’t align with my current setup.

I’ve encountered a scenario where a new record requires a slug derived from the label. To optimize the process and avoid redundant queries, I’ll verify if a slug already exists within the icon set. If it does, I’ll remove the matching slug portion and return the maximum value of the suffix. Subsequently, I’ll increment the suffix and use that value.

generate_slug.php
function generate_slug($db, $setId, $label) {
    $slug = url_safe_string($label);
    if(empty($slug)) $slug = uniqid();
    $sql = "SELECT COUNT(0)
    FROM IconSetTags
    INNER JOIN IconSets ON
        IconSetTags.setId = IconSets.id
    WHERE
      IconSets.slug = ?
      AND IconSetTags.slug = ?
    ";
    $total = $db->selectScalar($sql, 'ss', $setId, $slug);
    if ($total === false) {
        throw $db->get_last_exception();
    }
    if($total === 0) return $slug;
    $sql = "SELECT 
        MAX(SUBSTRING(IconSetTags.slug, ?))
    FROM IconSetTags
    INNER JOIN IconSets ON
        IconSetTags.setId = IconSets.id
    WHERE
      IconSets.slug = ?
      AND IconSetTags.slug LIKE ?
    ";
    $max = $db->selectScalar($sql, 'iss', str_length($slug), $setId, "$slug\_%");
    if ($max === false) {
        throw $db->get_last_exception();
    }
    $next = increment_slug($max);
    return "$slug$next";    
}
increment_slug.php
function increment_slug(string $slug)
{
    // latin1 sorting order of valid slug characters
    $charset = '0123456789-abcdefghijklmnopqrstuvwxyz';
    $base = strlen($charset);

    if (empty($slug)) {
        return '1';
    }

    $values = array_map(function ($char) use ($charset) {
        $pos = strpos($charset, $char);
        if ($pos === false) {
            $pos = $base - 1;
        }
        return $pos;
    }, str_split($slug));

    for ($i = count($values) - 1; $i >= 0; $i--) {
        $values[$i]++;
        if ($values[$i] < $charCount) {
            break;
        }
        $values[$i] = 0;
        if ($i === 0) {
            array_unshift($values, 0);
        }
    }

    return implode('', array_map(function ($index) use ($charset) {
        return $charset[$index];
    }, $values));
}

I’m primarily using a base-37 value for the suffix. While this approach is relatively straightforward, it seems to be functioning for now. However, it’s late, and I still have several API endpoints to convert. I’ll rest for the night and continue working on this tomorrow.

Discover more from Lewis Moten

Subscribe now to keep reading and get access to the full archive.

Continue reading