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
1npm install @ibrahim-bayer/strapi-http-toolkit
2yarn 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.
1export interface ProductModel extends BaseStrapiModel{
2 id: string;
3 documentId: string;
4 name: string;
5 product_status?: boolean;
6 category?: CategoryModel;
7 origin_country?: string;
8}
9
10export interface CategoryModel extends BaseStrapiModel{
11 id: number;
12 documentId: string;
13 name: string;
14 enabled: boolean;
15 children?: CategoryModel[];
16 products?: ProductModel[];
17}
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
1const filter: FilterOptions<ProductModel> = {
2 name: {
3 $contains: "laptop",
4 $startsWith: "Mac",
5 },
6 };
Relationship Populating
In this scenario let's populate categories of product and the children of each category.
1const populate: PopulateOptions<ProductModel> = {
2populate: {
3 category: {
4 populate: {
5 children: true,
6 }
7 }
8},
9};
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.
1const service = new GenericService<ProductModel>("/test", jwtToken);
2const response = await service.findOne("1234");
3const result = await service.findMany();
4
5const filters: FilterOptions<ProductModel> = {
6 product_status: { $eq: "active" },
7 price: { $gte: 100 }
8 };
9const result = await service.findMany(undefined, filters);
Creating a composite service.
1import { GenericService, FilterOptions, PopulateOptions } from '@ibrahim-bayer/strapi-http-toolkit';
2import { ProductModel } from '../types/strapi-models';
3
4export class ProductService {
5 private service: GenericService<ProductModel>;
6
7 constructor(baseUrl: string, jwtToken?: string) {
8 this.service = new GenericService<ProductModel>(`${baseUrl}/products`, jwtToken);
9 }
10
11 // Get all active products with categories
12 async getActiveProducts() {
13 const filters: FilterOptions<ProductModel> = {
14 product_status: { $eq: 'active' },
15 stock_quantity: { $gt: 0 }
16 };
17
18 const populate: PopulateOptions<ProductModel> = {
19 populate: {
20 category: {
21 populate: {
22 parent_category: true
23 }
24 }
25 }
26 };
27
28 return this.service.findMany(populate, filters);
29 }
30
31 // Search products by name and category
32 async searchProducts(searchTerm: string, categoryId?: string) {
33 const filters: FilterOptions<ProductModel> = {
34 $or: [
35 { name: { $containsi: searchTerm } },
36 { description: { $containsi: searchTerm } }
37 ]
38 };
39
40 if (categoryId) {
41 filters.category = { documentId: { $eq: categoryId } };
42 }
43
44 return this.service.findMany(undefined, filters);
45 }
46}
Step-4 Advance Filtering & Operations
A Complex filtering scenario.
1const complexFilters: FilterOptions<ProductModel> = {
2 // Price range
3 price: {
4 $gte: 100,
5 $lte: 500
6 },
7 // Multiple status values
8 product_status: {
9 $in: ['active', 'inactive']
10 },
11 // Nested relationship filter
12 category: {
13 enabled: { $eq: true },
14 parent_category: {
15 name: { $eq: 'Electronics' }
16 }
17 },
18 // Array field filtering
19 features: {
20 $contains: 'waterproof'
21 }
22};
Deep population scenario.
1const deepPopulate: PopulateOptions<ProductModel> = {
2 populate: {
3 category: {
4 populate: {
5 parent_category: true,
6 children: {
7 populate: {
8 products: {
9 populate: {
10 category: true
11 }
12 }
13 }
14 }
15 }
16 }
17 }
18};
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.
1const updateRequest : CrudRequestModel<ProductModel>{
2 data: {
3 name: "new value"
4 }
5}
Step-5 Testing Your Implementation
1import { ProductService } from '../services/product-service';
2import { ProductModel } from '../types/strapi-models';
3import { GenericService } from '@ibrahim-bayer/strapi-http-toolkit';
4
5// Mock the GenericService to isolate unit tests
6jest.mock('@ibrahim-bayer/strapi-http-toolkit');
7
8describe('ProductService', () => {
9 let productService: ProductService;
10 let mockGenericService: jest.Mocked<GenericService<ProductModel>>;
11
12 beforeEach(() => {
13 // Arrange - Reset mocks before each test
14 jest.clearAllMocks();
15 mockGenericService = {
16 findMany: jest.fn(),
17 findOne: jest.fn(),
18 create: jest.fn(),
19 update: jest.fn(),
20 delete: jest.fn(),
21 } as any;
22
23 (GenericService as jest.MockedClass<typeof GenericService>).mockImplementation(() => mockGenericService);
24 productService = new ProductService('http://localhost:1337/api', 'test-token');
25 });
26
27 describe('getActiveProducts', () => {
28 test('should fetch active products with correct filters and population', async () => {
29 // Arrange
30 const mockProducts: ProductModel[] = [
31 {
32 id: '1',
33 documentId: 'doc-1',
34 name: 'Wireless Headphones',
35 description: 'High-quality headphones',
36 price: 299.99,
37 product_status: 'active',
38 stock_quantity: 10,
39 features: ['bluetooth', 'noise-cancellation'],
40 category: {
41 id: 1,
42 documentId: 'cat-1',
43 name: 'Electronics',
44 slug: 'electronics',
45 enabled: true
46 }
47 }
48 ];
49
50 mockGenericService.findMany.mockResolvedValue({
51 data: mockProducts,
52 meta: { pagination: { total: 1 } }
53 });
54
55 // Act
56 const result = await productService.getActiveProducts();
57
58 // Assert
59 expect(mockGenericService.findMany).toHaveBeenCalledWith(
60 {
61 populate: {
62 category: {
63 populate: {
64 parent_category: true
65 }
66 }
67 }
68 },
69 {
70 product_status: { $eq: 'active' },
71 stock_quantity: { $gt: 0 }
72 }
73 );
74 expect(result.data).toHaveLength(1);
75 expect(result.data[0].product_status).toBe('active');
76 expect(result.data[0]).toHaveProperty('name');
77 expect(typeof result.data[0].price).toBe('number');
78 });
79
80 test('should handle empty results gracefully', async () => {
81 // Arrange
82 mockGenericService.findMany.mockResolvedValue({
83 data: [],
84 meta: { pagination: { total: 0 } }
85 });
86
87 // Act
88 const result = await productService.getActiveProducts();
89
90 // Assert
91 expect(result.data).toHaveLength(0);
92 expect(result.data).toBeInstanceOf(Array);
93 });
94 });
95
96 describe('searchProducts', () => {
97 test('should search products by name with correct filter structure', async () => {
98 // Arrange
99 const searchTerm = 'headphones';
100 const categoryId = 'cat-123';
101 const mockSearchResults: ProductModel[] = [
102 {
103 id: '2',
104 documentId: 'doc-2',
105 name: 'Bluetooth Headphones',
106 description: 'Wireless headphones with great sound',
107 price: 199.99,
108 product_status: 'active',
109 stock_quantity: 5,
110 features: ['bluetooth']
111 }
112 ];
113
114 mockGenericService.findMany.mockResolvedValue({
115 data: mockSearchResults,
116 meta: { pagination: { total: 1 } }
117 });
118
119 // Act
120 const result = await productService.searchProducts(searchTerm, categoryId);
121
122 // Assert
123 expect(mockGenericService.findMany).toHaveBeenCalledWith(
124 undefined,
125 {
126 $or: [
127 { name: { $containsi: searchTerm } },
128 { description: { $containsi: searchTerm } }
129 ],
130 category: { documentId: { $eq: categoryId } }
131 }
132 );
133 expect(result.data).toHaveLength(1);
134 expect(result.data[0].name).toContain('Headphones');
135 });
136
137 test('should search without category filter when categoryId is not provided', async () => {
138 // Arrange
139 const searchTerm = 'wireless';
140 mockGenericService.findMany.mockResolvedValue({
141 data: [],
142 meta: { pagination: { total: 0 } }
143 });
144
145 // Act
146 await productService.searchProducts(searchTerm);
147
148 // Assert
149 expect(mockGenericService.findMany).toHaveBeenCalledWith(
150 undefined,
151 {
152 $or: [
153 { name: { $containsi: searchTerm } },
154 { description: { $containsi: searchTerm } }
155 ]
156 }
157 );
158 });
159
160 test('should handle search errors appropriately', async () => {
161 // Arrange
162 const searchTerm = 'test';
163 const errorMessage = 'Network error';
164 mockGenericService.findMany.mockRejectedValue(new Error(errorMessage));
165
166 // Act & Assert
167 await expect(productService.searchProducts(searchTerm))
168 .rejects
169 .toThrow(errorMessage);
170 });
171 });
172});
Performance Considerations
Selective Population: Only populate relationships you need.
1const populate = {
2 populate: {
3 category: {
4 fields: ['name', 'slug']
5 }
6 }
7};
8
9// Avoid - over-populating
10const populate = {
11 populate: '*' // This fetches everything
12};
Efficient Filtering: Use database-level filtering instead of client-side.
1// Good - server-side filtering
2const filters: FilterOptions<ProductModel> = {
3 product_status: { $eq: 'active' },
4 price: { $gte: minPrice }
5};
6
7// Avoid - client-side filtering after fetch
8const allProducts = await service.findMany();
9const filtered = allProducts.data.filter(p => p.product_status === 'active');
Pagination: Always use pagination for large datasets.
1const paginatedResults = await service.findMany(populate, filters, {
2 pagination: {
3 page: 1,
4 pageSize: 20
5 }
6});
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.