Strapi HTTP Toolkit

Strapi-http-toolkit is designed to increase development velocity by providing a typesafe way to interact with your Strapi v5 API TypeScript applications.

Strapi HTTP Toolkit

Zero Inconsistency Typesafe Typescript Development with strapi-http-toolkit

  • meta-title: Type-Safe Strapi v5 Rest API with strapi-http-toolkit
  • meta-description: Strapi v5 development with typescript using strapi-http-toolkit for server-client data model inconsistencies.
  • Publish date:
  • Reviewers:

Introduction

Imagine a scenario; You are developing an e-commerce platform with Strapi as your backend. Your FE needs to filter products by category, price range, and availability. You write the filter object, deploy to staging, and suddenly a runtime errors. The backend expects product_status but your frontend sends 'productStatus'.

Sound familiar? These model inconsistencies between Strapi and TypeScript frontends cost developers hours of debugging time and create fragile applications that can potentially break in production. What if there was a way to catch these inconsistencies at compile time, before they even reach your test servers? Enter strapi-http-toolkit, a TypeScript library that eliminates server & client data model inconsistencies by providing compile time type safety for all your Strapi API interactions.

Strapi-http-toolkit is designed to increase development velocity by providing a typesafe way to interact with your Strapi v5 API TypeScript applications.

The Problem

Type Safety Gaps in Strapi Development

  • Data model mismatches between backend and frontend.
  • Runtime errors from incorrect filter syntax.
  • Complex relationship queries prone to breaking.
  • No type checking for API request/response structures.
  • Time wasted on debugging avoidable type errors.

The Cost of Inconsistency

  • Lost development time debugging type mismatches.
  • Production bugs that could have been caught at compile time.
  • Fragile codebases that break with backend changes.
  • Poor developer experience with no IDE autocompletion.

The Solution: strapi-http-toolkit

What it does?

  • Compile-time type safety for all Strapi API interactions.
  • Generic service classes with full TypeScript support, and much less boiler plate code.
  • Type-safe filtering, column selecting, and population options.
  • Consistent request/response models across your application.
  • Zero runtime overhead - all type checking happens at build time.
  • Code completion for all relations, filtering, and population.

Prerequisites

  • Strapi-5 driven BE.
  • Typescript based client project.

Using strapi-http-toolkit

Step-1 Installation

npm install @ibrahim-bayer/strapi-http-toolkit
yarn add @ibrahim-bayer/strapi-http-toolkit

Step-2 Define Your Interface Models

Let's start with an e-commerce example. We have a product, category relationship in database. A category can have multiple products and a category has children categories.

export interface ProductModel extends BaseStrapiModel{
  id: string;
  documentId: string;
  name: string;
  product_status?: boolean;
  category?: CategoryModel;
  origin_country?: string;
}

export interface CategoryModel extends BaseStrapiModel{
  id: number;
  documentId: string;
  name: string;
  enabled: boolean;
  children?: CategoryModel[];
  products?: ProductModel[];
}

How This Approach Works?

  • Extends BaseStrapiModel for common Strapi fields.
  • Uses exact field names from your Strapi schema.
  • Includes optional relationships with proper typing.
  • Supports nested relationships for complex queries.

Filtering

In this scenario let's filter products with active status and name contains "laptop" and starts with "Mac". By using strapi-http-toolkit we will generate a filter object and this is all. Fetch JSon Wrapper will convert your request to

const filter: FilterOptions<ProductModel> = {
        name: {
          $contains: "laptop",
          $startsWith: "Mac",
        },
      };

Relationship Populating

In this scenario let's populate categories of product and the children of each category.

const populate: PopulateOptions<ProductModel> = {
populate: {
  category: {
    populate: {
        children: true,
    }
  }
},
};

Ste-3 Creating a GenericService

Next scenario is creating a service for a model. Service methods are getting relevant filter and populate options as parameters and performing the Rest API calls.

const service = new GenericService<ProductModel>("/test", jwtToken);
const response = await service.findOne("1234");
const result = await service.findMany();

const filters: FilterOptions<ProductModel> = {
        product_status: { $eq: "active" },
        price: { $gte: 100 }
      };
const result = await service.findMany(undefined, filters);      

Creating a composite service.

import { GenericService, FilterOptions, PopulateOptions } from '@ibrahim-bayer/strapi-http-toolkit';
import { ProductModel } from '../types/strapi-models';

export class ProductService {
  private service: GenericService<ProductModel>;
  
  constructor(baseUrl: string, jwtToken?: string) {
    this.service = new GenericService<ProductModel>(`${baseUrl}/products`, jwtToken);
  }
  
  // Get all active products with categories
  async getActiveProducts() {
    const filters: FilterOptions<ProductModel> = {
      product_status: { $eq: 'active' },
      stock_quantity: { $gt: 0 }
    };
    
    const populate: PopulateOptions<ProductModel> = {
      populate: {
        category: {
          populate: {
            parent_category: true
          }
        }
      }
    };
    
    return this.service.findMany(populate, filters);
  }
  
  // Search products by name and category
  async searchProducts(searchTerm: string, categoryId?: string) {
    const filters: FilterOptions<ProductModel> = {
      $or: [
        { name: { $containsi: searchTerm } },
        { description: { $containsi: searchTerm } }
      ]
    };
    
    if (categoryId) {
      filters.category = { documentId: { $eq: categoryId } };
    }
    
    return this.service.findMany(undefined, filters);
  }
}

Step-4 Advance Filtering & Operations

A Complex filtering scenario.

const complexFilters: FilterOptions<ProductModel> = {
  // Price range
  price: { 
    $gte: 100, 
    $lte: 500 
  },
  // Multiple status values
  product_status: { 
    $in: ['active', 'inactive'] 
  },
  // Nested relationship filter
  category: {
    enabled: { $eq: true },
    parent_category: { 
      name: { $eq: 'Electronics' }
    }
  },
  // Array field filtering
  features: {
    $contains: 'waterproof'
  }
};

Deep population scenario.

const deepPopulate: PopulateOptions<ProductModel> = {
  populate: {
    category: {
      populate: {
        parent_category: true,
        children: {
          populate: {
            products: {
              populate: {
                category: true
              }
            }
          }
        }
      }
    }
  }
};

CRUD Operations with Type Safety

CrudRequestModel is used to perform partial or full data updates. It is handy when you are creating a model and want a basic definition without any values or you are partially updating a model. Below example is to generate a request to update only name of a product. This model is eliminating code duplication like: ProductPostModel, ProductUpdateModel, ProductResponse or any kind of non standard request, response models for your API calls.

const updateRequest : CrudRequestModel<ProductModel>{
    data: {
        name: "new value"
    }
}

Step-5 Testing Your Implementation

import { ProductService } from '../services/product-service';
import { ProductModel } from '../types/strapi-models';
import { GenericService } from '@ibrahim-bayer/strapi-http-toolkit';

// Mock the GenericService to isolate unit tests
jest.mock('@ibrahim-bayer/strapi-http-toolkit');

describe('ProductService', () => {
  let productService: ProductService;
  let mockGenericService: jest.Mocked<GenericService<ProductModel>>;

  beforeEach(() => {
    // Arrange - Reset mocks before each test
    jest.clearAllMocks();
    mockGenericService = {
      findMany: jest.fn(),
      findOne: jest.fn(),
      create: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    } as any;
    
    (GenericService as jest.MockedClass<typeof GenericService>).mockImplementation(() => mockGenericService);
    productService = new ProductService('http://localhost:1337/api', 'test-token');
  });

  describe('getActiveProducts', () => {
    test('should fetch active products with correct filters and population', async () => {
      // Arrange
      const mockProducts: ProductModel[] = [
        {
          id: '1',
          documentId: 'doc-1',
          name: 'Wireless Headphones',
          description: 'High-quality headphones',
          price: 299.99,
          product_status: 'active',
          stock_quantity: 10,
          features: ['bluetooth', 'noise-cancellation'],
          category: {
            id: 1,
            documentId: 'cat-1',
            name: 'Electronics',
            slug: 'electronics',
            enabled: true
          }
        }
      ];
      
      mockGenericService.findMany.mockResolvedValue({
        data: mockProducts,
        meta: { pagination: { total: 1 } }
      });

      // Act
      const result = await productService.getActiveProducts();

      // Assert
      expect(mockGenericService.findMany).toHaveBeenCalledWith(
        {
          populate: {
            category: {
              populate: {
                parent_category: true
              }
            }
          }
        },
        {
          product_status: { $eq: 'active' },
          stock_quantity: { $gt: 0 }
        }
      );
      expect(result.data).toHaveLength(1);
      expect(result.data[0].product_status).toBe('active');
      expect(result.data[0]).toHaveProperty('name');
      expect(typeof result.data[0].price).toBe('number');
    });

    test('should handle empty results gracefully', async () => {
      // Arrange
      mockGenericService.findMany.mockResolvedValue({
        data: [],
        meta: { pagination: { total: 0 } }
      });

      // Act
      const result = await productService.getActiveProducts();

      // Assert
      expect(result.data).toHaveLength(0);
      expect(result.data).toBeInstanceOf(Array);
    });
  });

  describe('searchProducts', () => {
    test('should search products by name with correct filter structure', async () => {
      // Arrange
      const searchTerm = 'headphones';
      const categoryId = 'cat-123';
      const mockSearchResults: ProductModel[] = [
        {
          id: '2',
          documentId: 'doc-2',
          name: 'Bluetooth Headphones',
          description: 'Wireless headphones with great sound',
          price: 199.99,
          product_status: 'active',
          stock_quantity: 5,
          features: ['bluetooth']
        }
      ];

      mockGenericService.findMany.mockResolvedValue({
        data: mockSearchResults,
        meta: { pagination: { total: 1 } }
      });

      // Act
      const result = await productService.searchProducts(searchTerm, categoryId);

      // Assert
      expect(mockGenericService.findMany).toHaveBeenCalledWith(
        undefined,
        {
          $or: [
            { name: { $containsi: searchTerm } },
            { description: { $containsi: searchTerm } }
          ],
          category: { documentId: { $eq: categoryId } }
        }
      );
      expect(result.data).toHaveLength(1);
      expect(result.data[0].name).toContain('Headphones');
    });

    test('should search without category filter when categoryId is not provided', async () => {
      // Arrange
      const searchTerm = 'wireless';
      mockGenericService.findMany.mockResolvedValue({
        data: [],
        meta: { pagination: { total: 0 } }
      });

      // Act
      await productService.searchProducts(searchTerm);

      // Assert
      expect(mockGenericService.findMany).toHaveBeenCalledWith(
        undefined,
        {
          $or: [
            { name: { $containsi: searchTerm } },
            { description: { $containsi: searchTerm } }
          ]
        }
      );
    });

    test('should handle search errors appropriately', async () => {
      // Arrange
      const searchTerm = 'test';
      const errorMessage = 'Network error';
      mockGenericService.findMany.mockRejectedValue(new Error(errorMessage));

      // Act & Assert
      await expect(productService.searchProducts(searchTerm))
        .rejects
        .toThrow(errorMessage);
    });
  });
});

Performance Considerations

Selective Population: Only populate relationships you need.

const populate = {
  populate: {
    category: {
      fields: ['name', 'slug']
    }
  }
};

// Avoid - over-populating
const populate = {
  populate: '*' // This fetches everything
};

Efficient Filtering: Use database-level filtering instead of client-side.

// Good - server-side filtering
const filters: FilterOptions<ProductModel> = {
  product_status: { $eq: 'active' },
  price: { $gte: minPrice }
};

// Avoid - client-side filtering after fetch
const allProducts = await service.findMany();
const filtered = allProducts.data.filter(p => p.product_status === 'active');

Pagination: Always use pagination for large datasets.

const paginatedResults = await service.findMany(populate, filters, {
  pagination: {
    page: 1,
    pageSize: 20
  }
});

Conclusion

strapi-http-toolkit was built with one goal in mind: Eliminate %100 percentage of the pain of mismatched models and unsafe API calls in client TypeScript projects. The library helps developers to focus on building features.

Ship with confidence: No more production bugs from type mismatches. Develop faster: Full IDE support with autocompletion and error detection. Scale reliably: Consistent patterns that work across your entire application. Maintain easily: Changes to your Strapi schema are caught at runtime.

The investment in setting up proper TypeScript interfaces pays dividends in reduced debugging time, fewer production issues, and a better developer experience.

Adopting this toolkit means faster development, fewer bugs, and peace of mind knowing your client application "mobile, web, etc.." and backend are always in sync. Give it a try, explore the GitHub repo.

How to start?

Install strapi-http-toolkit in your current project. Define your Strapi models using the patterns shown above. Migrate one service at a time to see immediate benefits. Expand to cover your entire API surface for complete type safety.

Community and Support

GitHub Repository: ibrahim-bayer/strapi-http-toolkit NPM Package: @ibrahim-bayer/strapi-http-toolkit Issues and Feature Requests: Use GitHub Issues for support. Contributions: Pull requests welcome for improvements and new features.

The future of Strapi development is type-safe, and strapi-http-toolkit is your handy tool to get there.

Frequently Asked Questions

Strapi HTTP Toolkit is a type-safe TypeScript API client for Strapi v5 APIs. It eliminates runtime errors through compile-time type checking for filtering, population, and CRUD operations.

Install the package via npm, then initialize the client with your Strapi base URL. The toolkit automatically generates types based on your Strapi schema, providing IntelliSense and type validation in your IDE.

The toolkit supports all standard CRUD operations (create, read, update, delete) plus Strapi-specific features like population, filtering, sorting, and pagination—all with full TypeScript type safety.

The toolkit primarily targets Strapi's REST API with type-safe wrappers. For GraphQL, consider using code generation tools like GraphQL Code Generator with Strapi's GraphQL plugin.

The toolkit catches type mismatches at compile time rather than runtime. If you misspell a field name or pass incorrect filter values, your IDE shows errors immediately, reducing bugs in production.