Skip to content

[API] Pagination, text search, and proximity filtering middleware #186

Closed

Description

@zakkiyyat

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

  1. OlaGreat commented on Jun 16, 2026

    @OlaGreat

    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.

  2. mattcrypted commented on Jun 27, 2026

    @mattcrypted

    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.

  3. yasinmuhd commented on Jun 27, 2026

    @yasinmuhd
    Contributor

    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.

  4. grantfox-oss commented on Jun 27, 2026

    @grantfox-oss

    🦊 GrantFox — @yasinmuhd has been assigned to this issue as part of the Official Campaign campaign!

    Next steps:

    1. Open a Pull Request referencing this issue (e.g., Closes #186)
    2. Your PR will be reviewed by the MixMatch-Inc maintainers

    Good luck! Track your progress on GrantFox.

  5. grantfox-oss commented on Jun 27, 2026

    @grantfox-oss

    🎉 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BACKENDBACKENDGrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official Campaignintermediate~2–3 day effortsprint-1Sprint 1: Restaurant & Menu Foundation

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions