Repository navigation
[API] Pagination, text search, and proximity filtering middleware #186
Description
Overview
Implement a reusable pagination and filtering layer in `apps/api/src/shared/` that can be applied to any list endpoint. The restaurant list and food item list endpoints (issues #180 and #181) should adopt it immediately. This middleware pattern will also be used by the discovery feed (Sprint 2).
File Location
```
apps/api/src/shared/
query/
paginate.ts # Cursor/offset pagination helper
text-search.ts # MongoDB $text search helper
geo-filter.ts # $geoNear / $near proximity filter
query.types.ts # Shared query param types
```
Pagination
Support offset-based pagination via query params:
```
GET /api/restaurants?page=1&limit=20
```
| Param | Default | Max |
|---|---|---|
| `page` | 1 | — |
| `limit` | 20 | 100 |
Response envelope (wrap all paginated list endpoints in this shape):
```json
{
"data": [...],
"pagination": {
"page": 1,
"limit": 20,
"total": 143,
"totalPages": 8,
"hasNextPage": true,
"hasPrevPage": false
}
}
```
The `paginate(model, filter, options)` helper should return this envelope from a single call.
Text Search
```
GET /api/restaurants?q=sushi
```
- Add a MongoDB text index on `restaurants.name` and `restaurants.description`
- When `q` is present, use MongoDB `$text: { $search: q }` and sort by `{ score: { $meta: 'textScore' } }`
- Text search and pagination must work together
Cuisine Tag Filter
```
GET /api/restaurants?cuisine=italian,pizza
```
- Accept a comma-separated list of `CuisineTag` values
- Filter using `{ cuisineTags: { $in: [...] } }`
Proximity Filter
```
GET /api/restaurants?lat=51.5074&lng=-0.1278&radius=5000
```
- `radius` is in metres, default 10 000 m (10 km), max 50 000 m
- Requires a 2dsphere index on `address.coordinates`
- Use MongoDB `$geoNear` aggregation or `$near` query operator
- When coordinates are not provided, skip this filter entirely (no error)
Acceptance Criteria
- `paginate()` helper is generic — it accepts any Mongoose model and filter object
- `page` and `limit` params are validated with Zod; invalid values default to safe values (not 500 errors)
- Text index is created in a Mongoose `createIndexes()` call within the restaurant model file (not a migration script)
- 2dsphere index is created on `address.coordinates` in the restaurant model
- Proximity filter is silently skipped when `lat`/`lng` are absent — never throws when they are omitted
- All three filters (text, cuisine, proximity) can be combined in a single request
- `GET /api/restaurants` and `GET /api/restaurants/:id/food-items` both use the `paginate()` helper
- The response envelope type is exported from `packages/shared/src/types/` for use on the web and mobile clients
Testing Requirements
- Pagination returns correct `totalPages` and `hasNextPage` values
- Requesting `page` beyond total returns empty `data` array (not 404)
- Text search returns restaurants matching the query
- Cuisine filter returns only matching restaurants
- Proximity filter returns restaurants within the radius when coordinates are provided
- All filters combined return the intersection correctly
Out of Scope
- Cursor-based pagination (offset is sufficient for this phase)
- Price range filtering on food items (Sprint 2 filter sheet)
Activity
OlaGreat commentedon Jun 16, 2026
I'd like to take this on.
I'm a software engineer with experience in blockchain, backend systems, and open-source contribution. I've reviewed the issue and I'm confident I can deliver a clean implementation with tests covering the acceptance criteria. Happy to follow the project's existing conventions.
mattcrypted commentedon Jun 27, 2026
I'm a backend engineer with experience building Node and TypeScript API middleware, query layers, and thoroughly tested services.
This issue aligns closely with my experience implementing reusable request middleware, pagination, and search and filtering for list endpoints. I'd be excited to build composable pagination, text search, and proximity filtering middleware, and help move this feature forward.
yasinmuhd commentedon Jun 27, 2026
I'm interested in taking on this issue. My approach is to understand the requirements thoroughly, identify edge cases early, and deliver clean, reliable implementations that minimize future technical debt.
I've worked on similar features across multiple projects and would be excited to contribute here as well.
grantfox-oss commentedon Jun 27, 2026
🦊 GrantFox — @yasinmuhd has been assigned to this issue as part of the Official Campaign campaign!
Next steps:
Closes #186)Good luck! Track your progress on GrantFox.
grantfox-oss commentedon Jun 27, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Official Campaign campaign!
@yasinmuhd's PR #196 was approved and merged by @zakkiyyat.
🏆 @yasinmuhd: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (118 total points). Track your full progress on GrantFox.
👏 Great work, @yasinmuhd! Keep contributing to MixMatch-Inc.