typescript 84 lines · 3 tabs

Parse a CSV Upload into Typed Rows with Per-Row Validation Errors

Shared by codesnips Aug 2026
3 tabs
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[];
}
3 files · typescript Explain with highlit

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

typescript
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

typescript reliability retry
by codesnips 2 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
typescript
export type Settled<R> =
  | { status: 'fulfilled'; value: R }
  | { status: 'rejected'; reason: unknown };

export interface ConcurrencyOptions {
  limit: number;

Simple concurrency limiter for batch operations

node concurrency async
by codesnips 2 tabs
typescript
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

react axios api
by Maya Patel 1 tab

Share this code

Here's the card — post it anywhere.

Parse a CSV Upload into Typed Rows with Per-Row Validation Errors — share card
Link copied