import { z } from "zod";
export const rowSchema = z.object({
email: z.string().email(),
name: z.string().min(1, "name is required"),
age: z.coerce.number().int().min(0, "age must be >= 0"),
signupDate: z.coerce.date(),
country: z.string().length(2, "country must be an ISO code").toUpperCase(),
});
export type ContactRow = z.infer<typeof rowSchema>;
export interface RowError {
rowNumber: number;
column: string;
message: string;
}
export interface ParseResult {
rows: ContactRow[];
errors: RowError[];
}
import { Readable } from "node:stream";
import { parse } from "csv-parse";
import { ZodError } from "zod";
import { rowSchema, ContactRow, RowError, ParseResult } from "./rowSchema";
function flattenZodError(err: ZodError, rowNumber: number): RowError[] {
return err.issues.map((issue) => ({
rowNumber,
column: issue.path.join(".") || "(row)",
message: issue.message,
}));
}
export async function parseCsvStream(input: Readable): Promise<ParseResult> {
const rows: ContactRow[] = [];
const errors: RowError[] = [];
const parser = input.pipe(
parse({ columns: true, trim: true, skip_empty_lines: true })
);
let rowNumber = 0;
for await (const record of parser) {
rowNumber += 1;
const result = rowSchema.safeParse(record);
if (result.success) {
rows.push(result.data);
} else {
errors.push(...flattenZodError(result.error, rowNumber));
}
}
return { rows, errors };
}
import { Router, Request, Response } from "express";
import multer from "multer";
import { Readable } from "node:stream";
import { parseCsvStream } from "./parseCsv";
import { saveContacts } from "./contactsRepository";
const upload = multer({ storage: multer.memoryStorage() });
export const importRouter = Router();
importRouter.post(
"/contacts/import",
upload.single("file"),
async (req: Request, res: Response) => {
if (!req.file) {
return res.status(400).json({ error: "file is required" });
}
const stream = Readable.from(req.file.buffer);
const { rows, errors } = await parseCsvStream(stream);
if (errors.length > 0) {
return res.status(422).json({ imported: 0, errors });
}
await saveContacts(rows);
return res.status(201).json({ imported: rows.length });
}
);
This snippet shows a common data-import task: taking a raw CSV upload and turning it into strongly-typed rows while collecting every validation problem per row, rather than failing on the first bad line. The goal is to give an import UI enough detail to tell a user "row 12, column email, invalid format" while still letting the good rows through.
In rowSchema.ts, the shape of a valid row is declared with zod. The schema uses z.coerce for age and signupDate so string cells from the CSV are converted to number and Date, and refinements enforce domain rules like a non-negative age. Exporting ContactRow via z.infer keeps the runtime schema and the compile-time type in sync — there is a single source of truth, so drift between the parser and downstream code is impossible. RowError and ParseResult model the two-channel outcome: an array of typed rows plus an array of structured errors.
parseCsv.ts does the mechanical work. It uses csv-parse in streaming mode with columns: true so each record arrives as a keyed object, and parseCsvStream walks records one at a time. Each raw record is run through rowSchema.safeParse, which never throws; on failure the ZodError is flattened into one RowError per offending field via flattenZodError, tagged with the 1-based rowNumber. Valid rows are pushed to rows, invalid ones only contribute errors, so a single malformed line cannot abort the whole import. Streaming keeps memory flat for large files because rows are processed as they are read instead of buffering the entire file.
import.controller.ts wires this into an Express endpoint. It pulls the uploaded buffer from multer, hands the stream to parseCsvStream, and branches on whether errors is empty. When there are errors it returns HTTP 422 with the structured list so the client can render inline messages; otherwise it forwards the clean rows to persistence and returns a count. A key trade-off here is partial success: the controller rejects the whole batch on any error, but because ParseResult carries both channels, switching to "import the valid rows and report the rest" is a one-line change. This pattern is worth reaching for whenever user-supplied tabular data must be validated field-by-field with actionable feedback.
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
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
Share this code
Here's the card — post it anywhere.