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 DateDate 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:
- DB returns a timestamp string
(e.g.,'2023-06-26 09:00:00'from PostgreSQL) - Driver coerces it into a
Dateobject
(pgturns timestamp columns intoDate) - You send it to the client via JSON
JSDategets 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); // oopsThis 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
Dateinstances 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 internallyBut "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) → JSDate(interpreted as local time)timestamp with time zone(OID 1184) → JSDate(converted to UTC)
This automatic conversion can cause subtle bugs because:
Dateobjects add complexity and hidden behavior in DTOstimestamp without time zonehas no timezone info, but JSDateassumes local timezone, leading to silent shifts.timestamp with time zoneconversion 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
| Problem | Why It Happens | How Strings Solve It |
|---|---|---|
| JSON serialization mismatch | Date → string → not Date | Strings stay strings |
| Timezone bugs | Local vs UTC vs DB zone | Explicit ISO string, no guessing |
| Validation pain | Date will auto-correct invalid values or fail silently | Easy schema/regex checks |
| Plain date mismatch | Plain Date vs Date Time mismatch, timezone shifts | Date stays date |
| Plain time mismatch | Plain Time vs Date Time mismatch, timezone shifts | Time stays time |
| ORM coercion issues | Auto parsing, wrong format | You control parsing |
| Testing overhead | Constructing new date object | Simple 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.
