Date and Time Handling with Claude Code: A Practical Guide
Use Claude Code to implement timezone-safe date handling, Intl formatting, DST tests, and DB storage policy.
Why Date and Time Handling Needs More Than a Prompt
Date and time bugs are easy to miss in review and expensive in production. A screen can look correct in Japan while failing during US daylight saving time, European month-end billing, Indian half-hour offsets, or a server/client timezone mismatch. If you ask Claude Code only to “implement date handling”, it may produce clean formatting while leaving storage policy, API contracts, and boundary tests unclear.
Use Claude Code as a pair engineer for the date/time specification, not only as a code generator. This guide focuses on web apps with reservations, billing, notifications, support SLAs, and dashboards that cross timezone boundaries. For the broader testing workflow, pair it with Claude Code testing strategies. For schema changes, read database migration with Claude Code.
Check official references before implementation. Use MDN Intl.DateTimeFormat for display behavior, TC39 Temporal and MDN Temporal for Temporal status and concepts, PostgreSQL Date/Time Types for database behavior, and Claude Code hooks when you want deterministic checks after edits.
Define the Vocabulary and Storage Policy First
Before asking Claude Code to write code, lock down the words your team uses. Otherwise, review comments drift into vague claims like “we store UTC, so it is safe” or “the product is Japan-only, so JST is fine”.
| Term | Plain meaning | Practical storage rule |
|---|---|---|
| instant | One exact moment globally, such as 2026-06-02T00:00:00Z | Store as a UTC-based timestamp |
| local date | A calendar date for a user or business, such as birthday or due date | Keep as YYYY-MM-DD, separate from time |
| wall clock time | The time a person sees on the clock, such as a 09:00 meeting | Store with an IANA timezone ID |
| IANA timezone | A region name such as Asia/Tokyo or America/New_York | Do not replace it with a fixed offset |
| DST | Daylight saving time; some local days have 23 or 25 hours | Test transition days explicitly |
In real projects, put these three rules in AGENTS.md or CLAUDE.md before asking for implementation.
- API timestamps for events that already happened must be ISO 8601 strings with
Zor an explicit offset. - Future scheduled work must keep
localDate,localTime, andtimeZoneas separate fields. - UI formatting must pass both
localeandtimeZonetoIntl.DateTimeFormat; never rely on runtime defaults.
The boundary should look like this.
flowchart LR
A["Client: local date/time input"] --> B["API contract: localDate + localTime + timeZone"]
B --> C["Server: validate and convert when needed"]
C --> D["Database: instant in timestamptz + original timeZone"]
D --> E["UI: format with Intl.DateTimeFormat"]
E --> A
Build a Safe Base with Intl.DateTimeFormat
For display, start with the standard Intl.DateTimeFormat API. MDN describes it as the built-in API for language-sensitive date and time formatting, and it accepts an explicit timeZone. Tell Claude Code not to use toLocaleString() without arguments and not to parse date-only strings with Date when the value means a local calendar day.
The following module is a dependency-free baseline. Put it in src/lib/date-policy.ts and reuse it from UI, API handlers, and tests.
// src/lib/date-policy.ts
export const TIME_POLICY = {
defaultLocale: 'en-US',
defaultTimeZone: 'America/New_York',
} as const;
type FormatOptions = {
locale?: string;
timeZone?: string;
includeWeekday?: boolean;
};
function toDate(input: string | Date): Date {
const date = input instanceof Date ? input : new Date(input);
if (!Number.isFinite(date.getTime())) {
throw new Error(`Invalid date value: ${String(input)}`);
}
return date;
}
export function toUtcIso(input: string | Date): string {
if (typeof input === 'string' && !/(Z|[+-]\d{2}:?\d{2})$/i.test(input)) {
throw new Error('Timestamp must include Z or an explicit UTC offset.');
}
return toDate(input).toISOString();
}
function dateParts(date: Date, timeZone: string): Record<string, string> {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(date);
return Object.fromEntries(
parts
.filter((part) => part.type !== 'literal')
.map((part) => [part.type, part.value]),
);
}
export function dayKeyInTimeZone(
input: string | Date,
timeZone = TIME_POLICY.defaultTimeZone,
): string {
const parts = dateParts(toDate(input), timeZone);
return `${parts.year}-${parts.month}-${parts.day}`;
}
export function formatInstant(
input: string | Date,
{
locale = TIME_POLICY.defaultLocale,
timeZone = TIME_POLICY.defaultTimeZone,
includeWeekday = true,
}: FormatOptions = {},
): string {
return new Intl.DateTimeFormat(locale, {
timeZone,
weekday: includeWeekday ? 'short' : undefined,
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
timeZoneName: 'short',
}).format(toDate(input));
}
The point is not to hide every date operation behind a utility. The point is to separate an instant, a local-day key, and a display label. Before Claude Code adds another helper, ask it to explain which category the new requirement belongs to.
Convert Future Local Times Explicitly
Intl.DateTimeFormat is good for output, but it is not the right tool for turning 2026-11-01 01:30 America/New_York into a safe UTC instant. For that job, choose one project-level library and verify the current docs. Luxon has explicit timezone APIs documented in its API docs. date-fns is strong for function-based date utilities, but timezone requirements should be checked against its current official docs. Temporal is Stage 4 at TC39, but you still need to check runtime support and any polyfill plan for your target browsers and Node version.
For a booking form, convert the user’s local date and time into the instant you store.
// src/lib/schedule-time.ts
import { DateTime } from 'luxon';
type LocalScheduleInput = {
localDate: string; // YYYY-MM-DD
localTime: string; // HH:mm
timeZone: string; // IANA time zone, for example "America/New_York"
};
export function scheduleToUtcIso(input: LocalScheduleInput): string {
const rawLocal = `${input.localDate}T${input.localTime}`;
const local = DateTime.fromISO(rawLocal, { zone: input.timeZone });
if (!local.isValid) {
throw new Error(local.invalidExplanation ?? `Invalid local time: ${rawLocal}`);
}
const roundTrip = local.setZone(input.timeZone).toFormat("yyyy-MM-dd'T'HH:mm");
if (roundTrip !== rawLocal) {
throw new Error(`Nonexistent local time in ${input.timeZone}: ${rawLocal}`);
}
const iso = local.toUTC().toISO({ suppressMilliseconds: true });
if (!iso) {
throw new Error(`Could not convert local time to UTC: ${rawLocal}`);
}
return iso;
}
This helper does not magically solve the repeated 01:00 hour when daylight saving time ends. Your product still needs a rule: choose the earlier offset, choose the later offset, or ask the user. Tell Claude Code to add tests for nonexistent local times, ambiguous local times, month ends, and leap days.
Separate Server, Client, and Database Boundaries
The most common bug is passing a value to Date without deciding whether the server or the browser timezone owns the interpretation. Browser inputs, API payloads, database columns, emails, and CSV exports are different boundaries.
Use at least these three concrete use cases when reviewing the design.
- Booking system: input uses the venue’s local time, storage uses a UTC instant, display uses either the viewer’s or venue’s timezone.
- Billing cutoff: “last day of the month at 23:59” is a local-date rule in the customer’s contract timezone; do not create next month by adding 24-hour chunks.
- Support SLA: business days, holidays, and office hours vary by region, so keep both the deadline instant and the human-readable local context.
With PostgreSQL, the official docs explain that timestamp with time zone input is converted to UTC and the original timezone is not retained. That means timestamptz alone cannot tell you that a user selected America/New_York. Store that separately when the product needs it.
create table scheduled_events (
id uuid primary key,
title text not null,
starts_at timestamptz not null,
original_time_zone text not null check (original_time_zone <> ''),
local_date date not null,
local_time time not null,
created_at timestamptz not null default now()
);
create index scheduled_events_starts_at_idx
on scheduled_events (starts_at);
Do not assume that inserting 2026-06-02T09:00:00+09:00 into a timestamp without time zone column keeps the offset meaningful. PostgreSQL determines the type first; for a value already determined to be without time zone, the timezone indication is ignored. Add “datetime column type matches the API contract” to Claude Code’s review checklist.
Write DST and Boundary Tests with Fixed Instants
Tests should not depend on “today”, the current clock, or the developer machine’s timezone. Fix the input as a UTC instant, then assert the expected local day or label. If you use Vitest, start with this file. For broader patterns, see Claude Code Vitest advanced.
// src/lib/date-policy.test.ts
import { describe, expect, it } from 'vitest';
import { dayKeyInTimeZone, formatInstant, toUtcIso } from './date-policy';
describe('date/time policy', () => {
it('requires an explicit offset for API timestamps', () => {
expect(() => toUtcIso('2026-06-02T09:00:00')).toThrow(/offset/);
expect(toUtcIso('2026-06-02T09:00:00+09:00')).toBe('2026-06-02T00:00:00.000Z');
});
it('calculates a local day key across the UTC date boundary', () => {
expect(dayKeyInTimeZone('2026-03-31T15:01:00Z', 'Asia/Tokyo')).toBe('2026-04-01');
expect(dayKeyInTimeZone('2026-04-01T00:30:00Z', 'America/Los_Angeles')).toBe('2026-03-31');
});
it('formats a DST transition instant in the requested time zone', () => {
const label = formatInstant('2026-03-08T07:30:00Z', {
locale: 'en-US',
timeZone: 'America/New_York',
});
expect(label).toMatch(/03:30|3:30/);
expect(label).toMatch(/EDT|GMT-4/);
});
});
The test intentionally allows either EDT or GMT-4, because ICU data can differ between environments. Local-day keys and UTC conversion results are business logic, so those expectations should stay strict.
Prevent Regressions with Claude Code Prompts and Hooks
Give Claude Code the constraints before implementation. A longer prompt is fine when it names the dangerous boundaries.
Before implementing date/time changes, read date-policy.ts and the database schema.
Constraints:
- API timestamps for completed events must be ISO strings with Z or an explicit offset
- Future bookings must keep localDate, localTime, and timeZone separate
- Intl.DateTimeFormat must always receive locale and timeZone
- Do not use new Date('YYYY-MM-DD') for local calendar dates
- Add Vitest cases for DST start, DST end, month end, and leap day
- Explain the PostgreSQL timestamp/timestamptz difference in the review notes
Done when:
- npm test -- --run date-time passes
- Updated API response examples are added to README
For teams, use Claude Code hooks to run deterministic checks after edits. The official docs show that hooks can run shell commands at lifecycle events. A date/time test hook can live in .claude/settings.json. For the full workflow, see Claude Code hooks guide.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npm test -- --run date-time"
}
]
}
]
}
}
Choose Libraries by the Type of Time You Handle
Do not choose a date library only by popularity. Choose it by the kind of time your product handles. Use Intl.DateTimeFormat for locale-aware display, date-fns for focused date utilities, Luxon for IANA timezone scheduling, and Temporal when you want future-facing types such as Instant, PlainDate, and ZonedDateTime. Even with Temporal at Stage 4, confirm target runtime support and polyfill policy before asking Claude Code to rewrite production code.
| Option | Best fit | Caveat |
|---|---|---|
| Intl | Locale-aware display with explicit timezone formatting | Not a local-time-to-instant conversion tool |
| date-fns | Pure date math, function-level imports, small utilities | Check current official docs for timezone needs |
| Luxon | IANA timezone scheduling, relative time, ISO conversion | Ambiguous DST times still need product rules |
| Temporal | Clear separation of Instant, PlainDate, and ZonedDateTime | Runtime compatibility and polyfill plan matter |
From a content and monetization angle, a generic library list is weak. Readers need to know what to store, where to convert, and what to test. If your team wants this policy embedded into CLAUDE.md, hooks, PR review, and migrations, the Claude Code training and consultation page is the natural next step. Solo developers can add the prompt above to the free cheat sheet and use it as a repeatable guardrail.
Hands-On Verification Note
Masa’s verification note: the most useful check was fixing a UTC instant and formatting it in Tokyo, New York, and Los Angeles, then asserting the local-day key across date boundaries. The easiest bug to reproduce was treating YYYY-MM-DD as a user’s local date, converting it to Date, and then seeing the previous day in another timezone. The policy in this article keeps local dates as strings and converts only stored instants and display labels, which makes Claude Code’s diff much easier to review.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.