These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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-toolkitStep-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.