const express = require('express');
const { toCsv } = require('./csvSerializer');
const { fetchOrders } = require('./orderRepository');
const router = express.Router();
const ORDER_COLUMNS = ['id', 'customer', 'total', 'status', 'createdAt'];
router.get('/reports/orders', async (req, res, next) => {
try {
const orders = await fetchOrders({ since: req.query.since });
res.format({
'application/json': () => {
res.json({ count: orders.length, data: orders });
},
'text/csv': () => {
const csv = toCsv(orders, ORDER_COLUMNS);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader(
'Content-Disposition',
'attachment; filename="orders.csv"'
);
res.send(csv);
},
default: () => {
res.status(406).json({
error: 'Not Acceptable',
supported: ['application/json', 'text/csv'],
});
},
});
} catch (err) {
next(err);
}
});
module.exports = router;
const RISKY_PREFIX = /^[=+\-@]/;
function escapeCell(value) {
if (value === null || value === undefined) return '';
let str = String(value);
// Neutralize CSV/formula injection for spreadsheet apps.
if (RISKY_PREFIX.test(str)) {
str = "'" + str;
}
if (/[",\n\r]/.test(str)) {
str = '"' + str.replace(/"/g, '""') + '"';
}
return str;
}
function toCsv(rows, columns) {
const header = columns.map(escapeCell).join(',');
const body = rows.map((row) =>
columns.map((col) => escapeCell(row[col])).join(',')
);
return [header, ...body].join('\r\n');
}
module.exports = { toCsv, escapeCell };
const express = require('express');
const reportsRouter = require('./reportsRouter');
const app = express();
app.use(express.json());
app.use('/api', reportsRouter);
app.use((err, req, res, next) => {
console.error(err);
if (res.headersSent) return next(err);
res.status(500).json({ error: 'Internal Server Error' });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`listening on ${port}`);
});
module.exports = app;
Content negotiation lets a single URL return different representations of the same resource based on what the client asks for. In reportsRouter, the route GET /reports/orders is defined once, but the response body is chosen at runtime from the request's Accept header. This avoids duplicating routes like /orders.json and /orders.csv, keeps the resource identity stable, and follows the HTTP spec's intent that a URI names a resource while the media type names its representation.
The core mechanism is Express's res.format, which inspects Accept and dispatches to the matching callback. In reportsRouter, the handler first loads data via fetchOrders, then calls res.format with keys for application/json, text/csv, and a default. Each branch sets the correct Content-Type automatically and, for CSV, adds Content-Disposition so browsers download a sensibly named file. When no branch matches, Express calls the default handler, which is written here to send a proper 406 Not Acceptable rather than silently guessing.
The CSV serialization lives in csvSerializer to keep the route thin and the formatting testable. toCsv builds a header row from an explicit columns list — relying on object key order is fragile — and escapes every field through escapeCell. That escaping is the part people get wrong: any cell containing a comma, quote, or newline must be wrapped in double quotes with internal quotes doubled, per RFC 4180. It also guards against CSV injection by prefixing values that begin with =, +, -, or @, which spreadsheet apps would otherwise interpret as formulas.
The app setup tab wires everything together and shows a subtle but important detail: express.json() is a body parser and has nothing to do with response negotiation, while ordering the router before the error handler matters. A trade-off of res.format is that it matches on media type only, not on quality-weighted preferences beyond Express's built-in req.accepts logic, so highly nuanced negotiation may need manual req.accepts() calls. For most APIs, though, this pattern cleanly serves machine clients JSON and analysts CSV from one endpoint, with correct status codes and headers.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
package api
import (
"io"
"net/http"
"os"
Safe multipart uploads using temp files (bounded memory)
package deps
import (
"crypto/tls"
"crypto/x509"
"net/http"
mTLS client configuration with custom root CA pool
Share this code
Here's the card — post it anywhere.