Build the CSV Export Route
Build the CSV Export Route
Fetch every contact
Open lib/contacts.ts and add:
export async function getAllContactsForExport(): Promise<Contact[]> {
const result = await pool.query(`SELECT * FROM contacts ORDER BY lower(name) ASC`);
return result.rows;
}
This deliberately ignores search and pagination — an export should always
include every contact, regardless of whatever filter happens to be active
on the Home page.
Build the CSV string
Open lib/actions.ts and add:
import { getAllContactsForExport } from "./contacts";
export async function exportCsvString(): Promise<string> {
const contacts = await getAllContactsForExport();
const header = "name,phone,email,company,notes,favorite";
const escape = (v: string | boolean | null) => {
const s = v === null || v === undefined ? "" : String(v);
return `"${s.replace(/"/g, '""')}"`;
};
const rows = contacts.map((c) =>
[c.name, c.phone, c.email, c.company, c.notes, c.favorite].map(escape).join(",")
);
return [header, ...rows].join("\n");
}
Every field gets wrapped in double quotes, with any existing double quote
inside the value doubled (" becomes "") — that’s the standard CSV
escaping rule, and it’s what lets a note like He said "call anytime"
round-trip correctly instead of breaking the file’s structure.
Build the Route Handler
Create app/api/export/route.ts:
import { exportCsvString } from "@/lib/actions";
export const dynamic = "force-dynamic";
export async function GET() {
const csv = await exportCsvString();
return new Response(csv, {
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": 'attachment; filename="contacts.csv"',
},
});
}
A Route Handler is a different kind of file from everything you’ve built
so far: instead of exporting a React component, route.ts exports a
function named after an HTTP method — GET, here — and returns a raw
Response. This is the right tool whenever a page needs to send something
that isn’t HTML: a file, a redirect with custom headers, a webhook
response, and so on.
Content-Disposition: attachment is what tells the browser to download
this response as a file named contacts.csv instead of trying to display
the CSV text as a page.
Test it
Visit http://localhost:3000/api/export directly in your browser. It
should download a contacts.csv file. Open it in a text editor or
spreadsheet app — you should see every contact you’ve created, one per
row, with the header row on top.
Checkpoint
Visiting /api/export downloads a correctly formatted CSV file containing
every contact, independent of any search or pagination state.