Skip to content

Admin Applicants — list endpoint with filters and pagination #486

@adityapat24

Description

@adityapat24

Goal

GET /api/v1/applicants returns a real paginated, filtered list of applicants from Mongo. The admin applicants table page now shows real data.

Tasks

Define the response shape

  • Confirm ApplicantSummary includes: _id, email, name (from responses), applicationStatus, decisionStatus, rsvpStatus, appSubmissionTime, lastSavedAt.
    • What this accomplishes: The table needs a compact row per applicant. Including just these fields keeps the response small even when listing 1000+ applicants.
  • The endpoint returns { rows: ApplicantSummary[], total: number, page: number, pageSize: number }.
    • What this accomplishes: total enables pagination controls. page and pageSize echo back so the client knows what it actually received.

Parse query parameters

  • Read query params: page (default 1), pageSize (default 25, max 100), sortBy (default appSubmissionTime), sortDir (default desc), status, decision, rsvp, search.
    • What this accomplishes: The frontend filter UI already sets these as URL params. The endpoint just needs to honor them.
  • Validate each: page is positive int, pageSize within bounds, sortBy is one of an allow-list of fields, sortDir is asc or desc, status/decision/rsvp are valid enum values or undefined.
    • What this accomplishes: Prevents bad input from generating malformed Mongo queries or DoS (a pageSize=10000000 request).
  • On invalid input, return 400 with a specific error.

Implement buildApplicantQuery

  • In src/lib/applicants/queries.ts, write buildApplicantQuery(filters: ApplicantFilters): Filter<ApplicantDoc>.
    • What this accomplishes: Translates filter inputs into a Mongo filter object. One function, easy to test.
  • If filters.status is set, add { applicationStatus: filters.status }. Same for decisionStatus and rsvpStatus.
  • If filters.search is set, build a case-insensitive regex matching the email field OR the name field. Use { $or: [{ email: regex }, { 'applicationResponses.name': regex }] }.
    • What this accomplishes: Admins can find an applicant by typing part of their name or email. Case-insensitive so capitalization doesn't matter.
  • Combine all conditions with $and (or just merge them — Mongo treats top-level fields as implicit AND).
    • What this accomplishes: Filters compose correctly when multiple are set.

Implement listApplicants

  • In src/lib/applicants/service.ts, write listApplicants({ filters, sort, page, pageSize }): Promise<{ rows, total, page, pageSize }>.
  • Call buildApplicantQuery(filters) to get the Mongo filter.
  • Run two parallel queries with Promise.all: applicant_data.countDocuments(filter) for total, and applicant_data.find(filter).sort({ [sort.by]: sort.dir === 'asc' ? 1 : -1 }).skip((page - 1) * pageSize).limit(pageSize).toArray() for rows.
    • What this accomplishes: Parallel queries mean the endpoint is fast even on large collections. Total count is needed for pagination; rows are the actual page.
  • Project the row results down to the ApplicantSummary fields. Extract name from applicationResponses.name (or whichever question ID holds it).
    • What this accomplishes: Avoids sending the entire application payload over the wire when only a summary is needed. Significant bandwidth savings at scale.
  • Return the assembled response.

Wire the API endpoint

  • Update src/app/api/v1/applicants/route.ts. Implement GET: parse and validate query params, call listApplicants, return JSON.
  • Add a comment // TODO: gate with requireAdmin() once Ticket 1 ships its helpers.

Index the collection

  • Add Mongo indexes on applicationStatus, decisionStatus, rsvpStatus, appSubmissionTime, and email.
    • What this accomplishes: Without indexes, the filter and sort operations do collection scans. With them, response time stays in single-digit milliseconds even on tens of thousands of records.
  • Document the indexes in a comment in src/lib/applicants/service.ts or in scripts/setup-indexes.ts if such a file exists.
    • What this accomplishes: Future devs setting up a new environment know which indexes are required.

Test via curl

  • Seed the database with ~20 applicants in various statuses (use or extend scripts/seed.ts).
  • curl localhost:3000/api/v1/applicants returns the first 25 (or 20, since you only have 20) sorted by submission time descending.
  • curl localhost:3000/api/v1/applicants?status=submitted returns only submitted applicants.
  • curl localhost:3000/api/v1/applicants?status=submitted&decision=admitted combines filters correctly.
  • curl localhost:3000/api/v1/applicants?search=alice returns applicants whose name or email contains "alice."
  • curl localhost:3000/api/v1/applicants?page=2&pageSize=5 returns rows 6-10.
  • curl localhost:3000/api/v1/applicants?sortBy=email&sortDir=asc returns rows sorted by email A-Z.
    • What this accomplishes: Each parameter is verified to work independently and in combination.

Test from the admin page

  • Start the dev server, go to /admin/applicants, change filters in the UI, change page, change sort. Confirm each interaction returns the right data.
    • What this accomplishes: Proves the URL-param-driven filter UI works against real persistence.

Unit tests

  • Test buildApplicantQuery produces the expected Mongo filter for each combination: status only, decision only, search only, all three, none.
  • Test that an invalid page=0 is rejected at the validation step.
  • Test that pageSize=999999 is clamped or rejected.
    • What this accomplishes: Catches regressions in filter logic and protects against DoS-style requests.

Definition of done

  • /admin/applicants loads real data from Mongo, paginated and filterable.
  • All query parameter combinations work correctly via curl.
  • The required Mongo indexes are documented and created.
  • Unit tests cover the filter-building and validation logic.

Metadata

Metadata

Assignees

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions