Rethinking Date Handling in DTOs: Why Strings Make More Sense in NodeJS

Data Transfer Objects (DTOs) are the contracts between client and the server. They need to be simple, serializable, and explicit. Yet over the years NodeJS developers casually drop JavaScript Date objects into DTOs—thinking it’s harmless and convenient.

It’s not.

Using Date in DTOs introduces subtle bugs, timezone inconsistencies, test fragility, and serialization mismatches. It also violates the basic principle of DTO design: no hidden behavior.

Here’s why you should stick to ISO 8601 strings instead—and what can go wrong if you don’t.


Problems with Using Date in DTOs

🧨 Serialization Pitfalls

const dto = { createdAt: new Date() };
const json = JSON.stringify(dto);
const parsed = JSON.parse(json);
// parsed.createdAt is a string, NOT a Date

Date gets serialized into ISO strings. But JSON.parse doesn’t convert it back. Unless you manually rehydrate it, you’re dealing with a plain string—but your types still say Date.

That’s a silent landmine.


🔁 Round-Trip Overhead: String → Date → String

In many Node.js stacks (e.g., with pg, Prisma, or TypeORM), here’s what happens by default:

  1. DB returns a timestamp string
    (e.g., '2023-06-26 09:00:00' from PostgreSQL)
  2. Driver coerces it into a Date object
    (pg turns timestamp columns into Date)
  3. You send it to the client via JSON
    JS Date gets serialized back into an ISO string

This means the value:

  • Starts as a string
  • Gets parsed into a Date (with all its JS quirks)
  • Gets serialized back to a string

This pointless transformation happens without your control, taking precious CPU cycles, unless you disable it.

And the worst part? The original string was already valid and ready to send to the client.


🧨 Date Validation Is a Lie

JavaScript Date is permissive to a fault. It will auto-correct invalid values or fail silently:

new Date("2023-02-31")
// → Fri Mar 03 2023 🤯 (auto-overflowed)

new Date("invalid")
// → Invalid Date (no exception thrown)

This makes it useless for input validation.

You can’t trust Date to tell you if user input was valid. You need a separate validation layer to verify strings like "2023-02-31" are truly valid ISO dates before parsing. Use tools like e.g. date-fns / ajv / luxon / zod / io-ts to vaidate dates.

🌀 Mutability and Data Integrity

JavaScript Date objects are mutable:

const dto = { createdAt: new Date() };
dto.createdAt.setFullYear(1999); // oops

This means any part of your code—or any library—can accidentally mutate a date object that was passed by reference.

DTOs are supposed to be immutable, serializable, and predictable. Date breaks all three principles.

By using strings instead, you guarantee immutability by default. No surprises, no side effects.

🧵 Broken API Contracts

Your API consumers send and receive strings—not Date objects. If your DTOs pretend Date is a valid payload type, you’ve created a lie.

This mismatch means developers must guess what format you expect, or reverse-engineer your serialization logic.

Explicit string fields remove ambiguity.


🧪 Easier Testing with String-Based Inputs

When your service or facade accepts dates as ISO strings instead of Date objects, tests become cleaner and easier to write:

  • No need to construct Date instances in every test case
  • No timezone ambiguity in test data
  • Inputs are plain, readable strings
test('this should work', async () => {
  // Instead of:
  myService.handle({
    createdAt: new Date('2023-01-01T00:00:00Z'),
  });

  // You write:
  myService.handle({
    createdAt: '2023-01-01T00:00:00Z',
  });
});

Using strings avoids noise and keeps your test data declarative and readable.

Date Fails for Time-Only Fields

Need to represent just a time – like "09:30:00"? Date is the wrong tool. Many DB drivers and ORMs auto-convert SQL TIME values into Date objects

// PostgreSQL via pg
row.opens_at.toISOString()
// → "1970-01-01T08:00:00.000Z" (shifted due to timezone)

// MySQL via mysql2
// → Same result

// TypeORM, Prisma
// → Maps `TIME` to JS Date internally

But "09:30:00" isn’t a timestamp. Converting it to Date adds a fake date and shifts time based on local zone.

📅 Storing Plain Dates Can Be Tricky

Databases often have a DATE type that stores just a date (e.g. 2023-07-08), no time or timezone.

But many drivers (like pg) convert DATE into JavaScript Date objects with midnight time in the server’s timezone or UTC:

row.birthdate instanceof Date // true
row.birthdate.toISOString()
// → "2023-07-07T22:00:00.000Z" (shifted if server is UTC-2)

This means:

  • You expected "2023-07-08" exactly
  • But your app sees a timestamp shifted by timezone offset, often the previous day in UTC
  • UI or logic that relies on just the date breaks (wrong day shown)

⚙️ Tell pg Driver to Stop Converting Timestamps to Dates

By default, the pg package converts PostgreSQL timestamps into JavaScript Date objects:

  • timestamp without time zone (OID 1114) → JS Date (interpreted as local time)
  • timestamp with time zone (OID 1184) → JS Date (converted to UTC)

This automatic conversion can cause subtle bugs because:

  • Date objects add complexity and hidden behavior in DTOs
  • timestamp without time zone has no timezone info, but JS Date assumes local timezone, leading to silent shifts.
  • timestamp with time zone conversion may not align with your app’s expected timezone handling.

Disable it like this:

import { types } from 'pg';

// Disable parsing to JS Dates — return raw strings
types.setTypeParser(1114, (val) => val); // timestamp
types.setTypeParser(1184, val =>         // timestamptz
  val.replace(' ', 'T').replace(/([+-]\d{2})(\d{2})$/, '$1:$2')
);
types.setTypeParser(1266, (val) => val); // timetz
types.setTypeParser(1082, (val) => val); // date
types.setTypeParser(1083, (val) => val); // time

Now you control how and when to parse.


✅ Recommended Approach

Keep DTOs string-based. Convert inwards to Date only where needed by your deeper layers.

import { IsISO8601 } from 'class-validator';

// DTO
class UserDto {
  @IsISO8601()
  createdAt: string;
};

// Service
const createdAt = new Date(dto.createdAt);

Or with more performant json schema validation via AJV.

import { JsonSchema, Required, Format } from '@code-net/json-schema-class';

// DTO
class JsonSchema {
  @Required()
  @Format('date-time')
  createdAt: string;
};

// Service
const createdAt = new Date(dto.createdAt);

🧠 TL;DR

ProblemWhy It HappensHow Strings Solve It
JSON serialization mismatchDate → string → not DateStrings stay strings
Timezone bugsLocal vs UTC vs DB zoneExplicit ISO string, no guessing
Validation painDate will auto-correct invalid values or fail silentlyEasy schema/regex checks
Plain date mismatchPlain Date vs Date Time mismatch, timezone shiftsDate stays date
Plain time mismatchPlain Time vs Date Time mismatch, timezone shiftsTime stays time
ORM coercion issuesAuto parsing, wrong formatYou control parsing
Testing overheadConstructing new date objectSimple ISO string

Conclusion

Don’t use Date in your DTOs.

You’re not gaining anything – and you’re introducing time-based bugs, fragile code, and opaque contracts. Use ISO 8601 strings. Control parsing where it matters.

Solutions architect, Node.JS developer, Knowledge Analyst

https://www.linkedin.com/in/bartosz-pasinski/

Leave a Reply

Your email address will not be published. Required fields are marked *