feat(backend): FastAPI endpoint with NDJSON streaming and CORS for Vite frontend
This commit is contained in:
+87
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Python module + CLI for parsing X12 837P professional claim files into one
|
||||
validated JSON per claim. Output is consumed by the Cyclone Vite/React
|
||||
frontend (later via a FastAPI service).
|
||||
frontend via a FastAPI service (see **REST API** below).
|
||||
|
||||
## Install
|
||||
|
||||
@@ -62,6 +62,92 @@ pytest -v
|
||||
The CLI smoke test against `docs/prodfiles/837p-from-axiscare/*.txt` is
|
||||
auto-skipped if no production files are present.
|
||||
|
||||
## REST API
|
||||
|
||||
The Cyclone Vite/React frontend (default dev origin `http://localhost:5173`)
|
||||
talks to the parser over HTTP. The API module is `cyclone.api:app`.
|
||||
|
||||
### Run the server
|
||||
|
||||
```bash
|
||||
uvicorn cyclone.api:app --reload --port 8000
|
||||
# or, equivalently:
|
||||
python -m cyclone serve
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ------ | ---------------- | -------------------------------------------------- |
|
||||
| GET | `/api/health` | Liveness probe: `{"status": "ok", "version": ...}` |
|
||||
| POST | `/api/parse-837` | Upload an X12 file, get parsed claims back |
|
||||
|
||||
`POST /api/parse-837` accepts `multipart/form-data` with a single `file`
|
||||
field. Optional query parameters:
|
||||
|
||||
- `payer` — `co_medicaid` (default) or `generic_837p`
|
||||
- `include_raw_segments` — `true` (default) or `false`
|
||||
- `strict` — `false` (default) or `true` (promote warnings to errors)
|
||||
|
||||
The response shape depends on the request's `Accept` header:
|
||||
|
||||
- `Accept: application/json` → a single `ParseResult`-shaped JSON object
|
||||
(envelope, claims, summary).
|
||||
- No `Accept` header (or anything that isn't `application/json`) →
|
||||
`application/x-ndjson` streaming, one JSON object per line in this order:
|
||||
`{"type": "envelope", "data": {...}}`, one `{"type": "claim", "data": {...}}`
|
||||
per claim, then `{"type": "summary", "data": {...}}`.
|
||||
|
||||
Error responses are JSON of the form `{"error": "...", "detail": "..."}`:
|
||||
|
||||
- `400` — file-level parse error (`CycloneParseError`) or unknown `payer`
|
||||
- `422` — at least one claim failed validation (body still includes the
|
||||
full `ParseResult` so the client can render the errors)
|
||||
- `500` — unexpected internal error
|
||||
|
||||
CORS is configured to allow `http://localhost:5173` with `GET` and `POST`
|
||||
and any headers.
|
||||
|
||||
### Example: upload + JSON response
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/parse-837 \
|
||||
-F "file=@./samples/co_837p.txt" \
|
||||
-H "Accept: application/json"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"envelope": { "sender_id": "...", "receiver_id": "...", ... },
|
||||
"claims": [ { "claim_id": "C1", ... }, { "claim_id": "C2", ... } ],
|
||||
"summary": { "total_claims": 2, "passed": 2, "failed": 0, ... }
|
||||
}
|
||||
```
|
||||
|
||||
### Example: stream NDJSON from the browser
|
||||
|
||||
```js
|
||||
const resp = await fetch("http://localhost:8000/api/parse-837", {
|
||||
method: "POST",
|
||||
body: formData, // FormData with a "file" entry
|
||||
});
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
for (const line of buffer.split("\n")) {
|
||||
if (!line) continue;
|
||||
const evt = JSON.parse(line);
|
||||
if (evt.type === "claim") renderClaim(evt.data);
|
||||
else if (evt.type === "summary") showSummary(evt.data);
|
||||
}
|
||||
buffer = buffer.slice(buffer.lastIndexOf("\n") + 1);
|
||||
}
|
||||
```
|
||||
|
||||
## Spec & plan
|
||||
|
||||
- Design: `docs/superpowers/specs/2026-06-19-cyclone-837p-parser-design.md`
|
||||
|
||||
Reference in New Issue
Block a user