A flexible, extensible Node.js API server built with Fastify that serves CSV data in multiple formats through HTTP content negotiation. The server automatically converts your data to the format requested by the client via the Accept header.
- Serve the same data in multiple formats (CSV, JSON, XLSX, Markdown, HTML, PDF, XML, GeoJSON, SQL)
- Clients request formats using HTTP
Acceptheaders Linkheaders in responses inform clients about available formats- Easy to add new formats by implementing the
FormatConverterinterface - Configure which formats are available for each resource
- Data is cached for optimal performance
- Full JSDoc type annotations
- Error handling, graceful shutdown, health checks
| Format | MIME Type | Extension | Description |
|---|---|---|---|
| CSV | text/csv |
csv |
Standard comma-separated values format |
| JSON | application/json |
json |
JSON array of objects |
| XLSX | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
xlsx |
Excel spreadsheet with auto-sized columns |
| Markdown | text/markdown |
md |
Markdown table with automatic numeric column alignment and padding |
| HTML | text/html |
html |
Fully styled HTML document with responsive table |
application/pdf |
pdf |
Multi-page PDF with headers, footers, and formatted tables | |
| XML | application/xml |
xml |
XML document with sanitized element names |
| GeoJSON | application/geo+json |
geojson |
GeoJSON FeatureCollection with automatic coordinate detection |
| SQL | text/x-sql |
sql |
SQL CREATE TABLE and INSERT statements with type inference |
npm installnpm startOr for development with auto-reload:
npm run devThe server starts on http://localhost:3000 by default.
The API uses HTTP content negotiation. Send an Accept header with your preferred MIME type:
curl http://localhost:3000/resources/mlb-ball-parks
# or explicitly:
curl -H "Accept: application/json" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: text/csv" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" \
http://localhost:3000/resources/mlb-ball-parks \
--output mlb-ball-parks.xlsxcurl -H "Accept: text/markdown" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: text/html" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: application/pdf" \
http://localhost:3000/resources/mlb-ball-parks \
--output mlb-ball-parks.pdfcurl -H "Accept: application/xml" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: application/geo+json" http://localhost:3000/resources/mlb-ball-parkscurl -H "Accept: text/x-sql" http://localhost:3000/resources/mlb-ball-parksAlternatively, you can specify the format using a file extension in the URL:
curl http://localhost:3000/resources/mlb-ball-parks.json
curl http://localhost:3000/resources/mlb-ball-parks.csv
curl http://localhost:3000/resources/mlb-ball-parks.xlsx
curl http://localhost:3000/resources/mlb-ball-parks.md
curl http://localhost:3000/resources/mlb-ball-parks.html
curl http://localhost:3000/resources/mlb-ball-parks.pdf
curl http://localhost:3000/resources/mlb-ball-parks.xml
curl http://localhost:3000/resources/mlb-ball-parks.geojson
curl http://localhost:3000/resources/mlb-ball-parks.sqlUse an OPTIONS request or check the Link header in any response:
# OPTIONS request
curl -X OPTIONS http://localhost:3000/resources/mlb-ball-parks
# Check Link header in response
curl -I http://localhost:3000/resources/mlb-ball-parksThe Link header contains all available formats:
Link: </resources/mlb-ball-parks.csv>; rel="alternate"; type="text/csv",
</resources/mlb-ball-parks.json>; rel="alternate"; type="application/json",
</resources/mlb-ball-parks.xlsx>; rel="alternate"; type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
...
Returns API information.
Response:
{
"name": "Multi-format Data Portal",
"version": "1.0.0",
"description": "Data portal API for serving data in multiple formats",
"endpoints": {
"resources": "/resources/:resourceName",
"health": "/health"
}
}Health check endpoint.
Response:
{
"status": "ok"
}Returns resource data in the requested format based on the Accept header.
Parameters:
resourceName(path): Name of the resource (e.g.,mlb-ball-parks)
Headers:
Accept: MIME type of desired format (optional, defaults to JSON)
Response:
- Content type varies based on
Acceptheader Linkheader includes all available formats- For XLSX and PDF:
Content-Dispositionheader suggests filename
Status Codes:
200: Success404: Resource not found406: No acceptable format found
Returns resource data in the specified format via file extension.
Parameters:
resourceName(path): Name of the resourceextension(path): File extension (e.g.,csv,json,xlsx,md,html,pdf,xml,geojson,sql)
Response: Same as GET /resources/:resourceName but format is determined by extension.
Returns available formats and HTTP methods for the resource.
Response:
Allowheader: Available HTTP methodsLinkheader: All available formats with their URLs
All format converters extend the FormatConverter base class and implement:
getMimeType(): Returns the primary MIME typegetExtension(): Returns the file extensiongetAlternativeMimeTypes(): Returns alternative MIME types (optional)getName(): Returns a human-readable nameconvert(data, options): Converts data array to Buffer
The FormatRegistry manages all available format converters and provides:
- Registration of new converters
- MIME type lookup
- Best match selection based on Accept headers
The ResourceRegistry manages data resources:
- Maps resource names to CSV files
- Configures which formats are available per resource
- Stores resource descriptions
Fastify plugin that:
- Parses
Acceptheaders (including quality values) - Selects the best matching format
- Generates
Linkheaders for format discovery
- Create a new converter class in
src/lib/formats/:
import { FormatConverter } from "./base.js";
export class MyFormatConverter extends FormatConverter {
getMimeType() {
return "application/my-format";
}
getExtension() {
return "myformat";
}
getName() {
return "My Format";
}
async convert(data, options = {}) {
// Convert data array to your format
const output = /* your conversion logic */;
return Buffer.from(output, "utf-8");
}
}- Register it in
src/lib/formats/registry.js:
import { MyFormatConverter } from "./myformat.js";
// In the constructor, add to defaultFormats:
this.defaultFormats = [
// ... existing formats
new MyFormatConverter(),
];- The format is now available for all resources.
Edit src/lib/resources.js and add to the constructor:
this.register("myresource", {
csvPath: "public/myresource.csv",
availableFormats: formatRegistry.getAllConverters(), // or specify specific formats
description: "Description of my resource",
});Place your CSV file in src/public/ and it will be automatically loaded and cached.
npm start: Start the production servernpm run dev: Start with auto-reload (usesnode --watch)npm run lint: Run ESLintnpm run format: Format code with Prettiernpm run types: Run TypeScript type checking (on JSDoc annotations)
PORT: Server port (default:3000)HOST: Server host (default:0.0.0.0)LOG_LEVEL: Logging level (default:info)NODE_ENV: Environment (developmentenables pretty logging)
MIT