⚡ Add Filters to Your Symfony API in 3 Minutes Flat
Stop writing the same filter for every endpoint. There’s a better way.
⚡ Add Filters to Your Symfony API in 3 Minutes Flat
Stop writing the same filter for every endpoint. There’s a better way.
Photo by sehoon ye on Unsplash
😩 The Problem We All Know Too Well
You’re building a REST API with Symfony. Your client needs to filter users by name, status, age range, email domain… You know the drill.
So you start writing this:
public function list(Request $request): JsonResponse
{
$qb = $this->userRepository->createQueryBuilder('u');
if ($request->query->has('firstname')) {
$qb->andWhere('u.firstname = :firstname')
->setParameter('firstname', $request->query->get('firstname'));
}
if ($request->query->has('status')) {
$qb->andWhere('u.status = :status')
->setParameter('status', $request->query->get('status'));
}
if ($request->query->has('min_age')) {
$qb->andWhere('u.age >= :min_age')
->setParameter('min_age', $request->query->get('min_age'));
}
// ... 50 more lines of this 😭
}
For every. single. endpoint. Just copy-paste spaghetti that grows with every new requirement.

🎯 What if You Could Do This Instead?
#[Route('/api/users', methods: ['GET'])]
#[ApiFilter(name: 'firstname', allowedTypes: [FilterType::Eq->value, FilterType::Like->value])]
#[ApiFilter(name: 'status', enumClass: UserStatus::class)]
#[ApiFilter(name: 'age', allowedTypes: [FilterType::Gte->value, FilterType::Lte->value])]
#[ApiFilter(name: 'deleted_at', allowedTypes: [FilterType::IsNull->value])]
public function list(Filters $filters): JsonResponse
{
$users = $this->userRepository->findByFilters($filters);
return $this->json($users);
}
That’s it. No manual parsing. No parameter binding. No validation code. Just declare what you allow, and the bundle handles the rest.

📦 Meet isma/api-filters-bundle
**isma/api-filters-bundle** is a Symfony bundle that:
- 🏷️ Reads
#[ApiFilter]attributes from your controller methods - ✅ Parses & validates query string filters automatically
- 🔧 Applies them to your Doctrine QueryBuilder.
It ships with multiple filters types and is fully extensible.
🚀 Setup in 3 Minutes
Minute 1 — Install
composer require isma/api-filters-bundle
Minute 2 — Declare Your Filters
Add #[ApiFilter] attributes to your controller action:
use Isma\ApiFiltersBundle\Attribute\ApiFilter;
use Isma\ApiFiltersBundle\ValueObject\Filters;
use Isma\ApiFiltersBundle\ValueObject\FilterType;
final class ProductController
{
#[Route('/api/products', methods: ['GET'])]
#[ApiFilter(name: 'name', allowedTypes: [FilterType::Like->value, FilterType::StartWith->value])]
#[ApiFilter(name: 'price', allowedTypes: [FilterType::Gte->value, FilterType::Lte->value])]
#[ApiFilter(name: 'category', allowedTypes: [FilterType::Eq->value])]
#[ApiFilter(name: 'discontinued_at', allowedTypes: [FilterType::IsNull->value])]
public function list(Filters $filters): JsonResponse
{
// $filters is automatically resolved from the query string 🪄
}
}
The Filters object is injected via Symfony's ValueResolverInterface — no manual Request parsing needed.
Minute 3 — Apply to Your Query
use Isma\ApiFiltersBundle\Filter\FilterApplierInterface;
final class ProductRepository
{
public function __construct(
private FilterApplierInterface $filterApplier,
) {}
public function findByFilters(Filters $filters): array
{
$qb = $this->createQueryBuilder('p');
$this->filterApplier->apply($qb, $filters, [
'name' => 'p.name',
'price' => 'p.price',
'category' => 'p.category',
'discontinued_at' => 'p.discontinuedAt',
]);
return $qb->getQuery()->getResult();
}
}
The mapping array ('name' => 'p.name') connects the public filter names from the URL to your actual Doctrine columns. That's your security layer — only mapped fields can be filtered.
Now hit your API:
GET /api/products?filters[name][like]=keyboard
GET /api/products?filters[price][gte]=50&filters[price][lte]=200
GET /api/products?filters[category][eq]=electronics

⏱️ Three minutes. No extra code. Completely validated.
Each filter type lives in its own class. Want to add an x-between filter? Simply create it:
use Doctrine\ORM\QueryBuilder;
use Isma\ApiFiltersBundle\Filter\FilterStrategyInterface;
final class BetweenFilterStrategy implements FilterStrategyInterface
{
public function getType(): string
{
return 'x-between';
}
public function apply(
QueryBuilder $queryBuilder,
string $column,
mixed $value,
string $parameterName,
): void {
$queryBuilder
->andWhere(sprintf(
'%s BETWEEN :%s_min AND :%s_max',
$column,
$parameterName,
$parameterName,
))
->setParameter($parameterName . '_min', $value[0])
->setParameter($parameterName . '_max', $value[1]);
}
}
That’s all. ✅ No service registration. ✅ No YAML. ✅ No XML.
It’s automatically discovered via Symfony autoconfiguration.
Example request:
GET /api/products?filters[price][x-between][]=100&filters[price][x-between][]=500
✍️ Conclusion
If you’re building a custom API with Symfony and don’t need the full API Platform ecosystem, this bundle is for you. Lightweight, declarative, and free from unnecessary complexity — just filtering, nothing more.
The project is open source and contributions are welcome. Whether it’s a new filter, a bug fix, or an improvement idea, feel free to open an issue or submit a PR on GitHub. Every contribution matters.
If this article helped you, give it a 👏 and follow me for more Symfony tips!
메타데이터
- post_id
- 35f1d3e71c05
- slug
- add-filters-to-your-symfony-api-in-3-minutes-flat-35f1d3e71c05
- url
- https://medium.com/@ZeroCool001/add-filters-to-your-symfony-api-in-3-minutes-flat-35f1d3e71c05
- canonical_url
- https://medium.com/@ZeroCool001/add-filters-to-your-symfony-api-in-3-minutes-flat-35f1d3e71c05
- author_url
- https://medium.com/@ZeroCool001
- status
- ok
- fetched_at
- 2026-07-30 16:09:00