Date handling is notoriously tricky. This guide covers best practices for working with dates across timezones.
Golden Rules#
- Store in UTC: Always store dates in UTC in your database
- Convert on display: Convert to user's timezone only for display
- Use ISO 8601: For API communication
Storing Dates#
1// Always store as UTC
2const now = new Date(); // Already in UTC internally
3
4// PostgreSQL
5// Use TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ)
6await db.query(`
7 INSERT INTO events (name, starts_at)
8 VALUES ($1, $2)
9`, [name, now.toISOString()]);Displaying Dates#
1// Convert to user's timezone for display
2function formatForUser(date: Date, timezone: string): string {
3 return new Intl.DateTimeFormat('en-US', {
4 timeZone: timezone,
5 dateStyle: 'medium',
6 timeStyle: 'short',
7 }).format(date);
8}
9
10formatForUser(new Date(), 'America/New_York');
11// "Jan 15, 2024, 3:30 PM"
12
13formatForUser(new Date(), 'Europe/London');
14// "Jan 15, 2024, 8:30 PM"Using date-fns with Timezones#
1import { format, parseISO } from 'date-fns';
2import { formatInTimeZone, toZonedTime } from 'date-fns-tz';
3
4// Parse ISO string (always UTC)
5const date = parseISO('2024-01-15T15:30:00Z');
6
7// Format in specific timezone
8const formatted = formatInTimeZone(
9 date,
10 'America/New_York',
11 'MMM d, yyyy h:mm a zzz'
12);
13// "Jan 15, 2024 10:30 AM EST"
14
15// Get date in specific timezone for calculations
16const nyDate = toZonedTime(date, 'America/New_York');API Communication#
1// Always use ISO 8601 format
2interface Event {
3 id: string;
4 name: string;
5 startsAt: string; // "2024-01-15T15:30:00Z"
6}
7
8// Client sends
9const payload = {
10 startsAt: selectedDate.toISOString(),
11};
12
13// Server responds
14const response = {
15 startsAt: event.starts_at.toISOString(),
16};Common Pitfalls#
1// ❌ Don't create dates from strings without timezone
2new Date('2024-01-15'); // Parsed as local midnight, not UTC!
3
4// ✅ Use ISO format with timezone
5new Date('2024-01-15T00:00:00Z'); // Explicitly UTC
6
7// ❌ Don't compare dates as strings
8'2024-01-15' < '2024-02-01'; // Works but fragile
9
10// ✅ Compare as Date objects
11new Date(a).getTime() < new Date(b).getTime();
12
13// ❌ Don't assume user's timezone
14const localTime = new Date().toLocaleString(); // Uses browser timezone
15
16// ✅ Explicitly specify timezone
17const userTime = new Intl.DateTimeFormat('en-US', {
18 timeZone: user.timezone,
19}).format(date);Storing User Timezone#
1// Get user's timezone
2const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
3// "America/New_York"
4
5// Store in user profile
6await db.users.update({
7 where: { id: userId },
8 data: { timezone: userTimezone },
9});Date Calculations#
1import { addDays, startOfDay, endOfDay } from 'date-fns';
2import { zonedTimeToUtc } from 'date-fns-tz';
3
4// Get start of day in user's timezone
5function getStartOfDayUTC(date: Date, timezone: string): Date {
6 const zonedDate = toZonedTime(date, timezone);
7 const startOfZonedDay = startOfDay(zonedDate);
8 return zonedTimeToUtc(startOfZonedDay, timezone);
9}
10
11// Query for events "today" in user's timezone
12const todayStart = getStartOfDayUTC(new Date(), 'America/New_York');
13const todayEnd = getStartOfDayUTC(addDays(new Date(), 1), 'America/New_York');
14
15const events = await db.events.findMany({
16 where: {
17 startsAt: {
18 gte: todayStart,
19 lt: todayEnd,
20 },
21 },
22});Testing Dates#
1import { vi } from 'vitest';
2
3// Mock current time
4vi.useFakeTimers();
5vi.setSystemTime(new Date('2024-01-15T12:00:00Z'));
6
7// Test
8expect(getCurrentTime()).toBe('2024-01-15T12:00:00.000Z');
9
10vi.useRealTimers();Store UTC, display local, use ISO 8601, and always be explicit about timezones.