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

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 Solution: strapi-http-toolkit
npm install @ibrahim-bayer/strapi-http-toolkit
yarn add @ibrahim-bayer/strapi-http-toolkitLet'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?
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",
},
};In this scenario let's populate categories of product and the children of each category.
const populate: PopulateOptions<ProductModel> = {
populate: {
category: {
populate: {
children: true,
}
}
},
};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);
}
}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'
}
};const deepPopulate: PopulateOptions<ProductModel> = {
populate: {
category: {
populate: {
parent_category: true,
children: {
populate: {
products: {
populate: {
category: true
}
}
}
}
}
}
}
};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"
}
}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);
});
});
});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
}
});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.
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.