---
title: "Export Endpoints — Passing Selection State from Frontend to Backend"
aliases: [export-filter-pattern, variant-ids-export, filtered-export, csv-pdf-selection]
tags: [api-design, frontend, backend, export, pdf, csv, fastapi, react]
sources:
  - "daily/2026-04-20.md"
created: 2026-04-20
updated: 2026-04-20
---

# Export Endpoints — Passing Selection State from Frontend to Backend

When a UI has a selection mechanism (checkboxes, multi-select) and also has export actions (PDF, CSV download), selection state must be explicitly passed to the backend export endpoints. If it is not, server-side exports always process the full dataset regardless of what the user selected — the frontend filter is cosmetic only.

## Key Points

- **Frontend-only selection is invisible to the backend** — a `selectedVariants` state variable in React has no effect on what `GET /export.csv` or `POST /contact-sheet` returns
- **POST with JSON body** is the correct pattern for PDF/contact-sheet exports: `{ "variant_ids": ["id1", "id2"] }`
- **Query parameters** are the correct pattern for GET-based CSV exports: `?variant_ids=id1,id2,id3`
- The backend exporter must filter before rendering — passing IDs to the endpoint is useless if the renderer ignores them and fetches all records from the banner set
- Show selected count in the UI before generating (`Exporting 3 of 12 variants`) — makes the filter visible to the user and confirms the IDs are being passed

## Details

### The Anti-Pattern

A common mistake when building export features: the frontend has a multi-select UI, but the export buttons call backend endpoints that fetch all variants from the banner set independently of any selection. The selection state in `BannerEditor` (or equivalent component) is never communicated to the server.

The user selects 3 of 12 variants and clicks "Export PDF" — the downloaded PDF contains all 12. The fix is architectural: the export endpoint must accept an ID list from the frontend.

### API Design for Filtered Exports

**PDF / Contact Sheet (POST):**
```python
# FastAPI endpoint
@router.post("/contact-sheet")
async def generate_contact_sheet(body: ContactSheetRequest):
    variants = await get_variants_by_ids(body.variant_ids)
    return render_pdf(variants)

class ContactSheetRequest(BaseModel):
    variant_ids: list[str]
```

```js
// React — pass selected IDs in request body
const response = await fetch('/contact-sheet', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ variant_ids: selectedVariantIds }),
});
```

POST is appropriate for PDF generation because: (1) the ID list can be arbitrarily long, (2) it triggers a side effect (rendering), (3) the response is a binary file, not a resource representation.

**CSV (GET with query params):**
```python
# FastAPI endpoint
@router.get("/export.csv")
async def export_csv(variant_ids: str = Query(...)):
    ids = variant_ids.split(",")
    variants = await get_variants_by_ids(ids)
    return generate_csv(variants)
```

```js
// React — encode IDs as comma-separated query parameter
const params = new URLSearchParams({ variant_ids: selectedVariantIds.join(',') });
window.location.href = `/export.csv?${params}`;
```

GET is appropriate for CSV because it's a download (the browser can handle it directly via `window.location.href` or an anchor tag), and the ID list is typically short enough for a query string.

### Backend Filter Before Render

The exporter must filter on the IDs received — fetching all variants and filtering in memory works but is wasteful at scale. The correct approach is a database query that filters by ID list:

```python
async def get_variants_by_ids(ids: list[str]):
    return await db.variants.find({"_id": {"$in": ids}}).to_list(None)
```

### UX: Show Selection Count Before Export

Before generating, show the user how many variants will be exported:

```jsx
<Button onClick={handleExport}>
  Export PDF ({selectedVariantIds.length} variant{selectedVariantIds.length !== 1 ? 's' : ''})
</Button>
```

This makes the filter state explicit in the UI and catches the common mistake of exporting with an empty selection.

## Related Concepts

- [[wiki/concepts/fastapi-mongodb-role-migration]] — FastAPI backend patterns including route design and Pydantic models
- [[wiki/concepts/openai-max-completion-tokens]] — another case of backend API parameter mismatch causing silent wrong behavior
- [[wiki/tech-patterns/_index]] — FastAPI and React/Vite tech patterns used in Oliver Agency projects

## Sources

- [[daily/2026-04-20.md]] — Barclays banner builder app (`optical-dev.oliver.solutions`): `POST /contact-sheet` updated to accept `{ variant_ids }` body; `GET /export.csv` updated to accept `?variant_ids=` query param; backend exporter filters before rendering; ExportPage shows selected variant count; pushed as commit `3d2893a`
