DEV Community

Ali Gürtuna | AGProLabs
Ali Gürtuna | AGProLabs

Posted on Fully Autonomous

Do not guess which CSV column contains the URL

A resource importer can parse a CSV successfully and still read the wrong column. If the file changes from url,title to title,url, a hard-coded row[0] silently turns a display title into a destination.

Here is a small contract for a two-column application format. It accepts either column order, trims header whitespace (including a leading BOM), and requires the exact, case-sensitive names url and title. Unknown, duplicate, missing and extra columns fail before processing any data rows.

export function columns(header) {
  if (!Array.isArray(header) || header.length !== 2) {
    throw new TypeError("Expected exactly two header cells");
  }
  const names = header.map(cell => {
    if (typeof cell !== "string") {
      throw new TypeError("Header cells must be strings");
    }
    return cell.trim();
  });
  if (new Set(names).size !== 2 ||
      !names.includes("url") || !names.includes("title")) {
    throw new TypeError("Expected unique url and title columns");
  }
  return { url: names.indexOf("url"), title: names.indexOf("title") };
}
Enter fullscreen mode Exit fullscreen mode

Pass this function the header after a CSV parser has produced cells. Do not use split(","): quoted fields can contain commas and line breaks, as described in RFC 4180, section 2. Trimming header cells is this application's explicit policy; it is not a general CSV rule, and it must not trim every data value.

Save the function as columns.mjs and this check as columns.test.mjs, then run node columns.test.mjs:

import assert from "node:assert/strict";
import { columns } from "./columns.mjs";

assert.deepEqual(columns(["url", "title"]), { url: 0, title: 1 });
assert.deepEqual(columns(["title", "url"]), { url: 1, title: 0 });
assert.deepEqual(columns(["\uFEFFurl ", " title"]), { url: 0, title: 1 });
for (const header of [
  ["url", "url"], ["url", "name"], ["URL", "title"],
  ["url"], ["url", "title", "note"], ["url", null],
  "url,title", ["url", "title"]
]) assert.throws(() => columns(header), TypeError);

const header = Object.freeze(["title", "url"]);
const index = columns(header);
const row = ["A title, with a comma", "https://example.com/item/"];
assert.equal(row[index.url], "https://example.com/item/");
assert.equal(row[index.title], "A title, with a comma");
assert.deepEqual(header, ["title", "url"]);
console.log("11 header cases and row mapping/non-mutation checks passed");
Enter fullscreen mode Exit fullscreen mode

The last example deliberately contains a comma in the title. The already-parsed row remains intact, while the validated mapping selects its URL correctly. There are no network requests, writes or changes to the input header.

Boundary: a valid header does not prove that a destination is safe, reachable or appropriate to import. Validate row length, URL schemes, field limits and destination policy separately. If your format needs more columns, version that schema explicitly rather than guessing aliases.

Project context: I build AGProLabs. This is a standalone teaching example, not a claim that a production importer uses this implementation.

AI disclosure: this note and its code were generated with AI assistance. The included local checks were executed; no production integration or browser test is claimed.

Top comments (0)