DEV Community

1xApi
1xApi

Posted on • Originally published at 1xapi.com

How to Use the ES2026 Temporal API in Node.js REST APIs (2026 Guide)

After 9 years in development and countless TC39 meetings, the JavaScript Temporal API officially reached Stage 4 on March 11, 2026, locking it into the ES2026 specification. That means it's no longer a proposal — it's the future of date and time handling in JavaScript, and you should start using it in your Node.js APIs today.

If you've ever shipped a date-related bug in production — DST edge cases, wrong timezone conversions, silent mutation bugs from Date.setDate() — you're not alone. The Date object was designed in 1995, copied from Java, and has been causing developer pain ever since. Temporal is the fix.

This guide covers how to use the ES2026 Temporal API in Node.js REST APIs with practical, real-world patterns: storing timestamps correctly, comparing durations, handling multi-timezone scheduling, and returning ISO 8601 dates from your endpoints.

What's Wrong with Date in 2026?

Let's be blunt. The JavaScript Date object is broken by design:

// Classic confusion: is this UTC or local?
const d = new Date('2026-04-01');
console.log(d.getDate()); // Could be March 31 in UTC-5 timezones!

// Mutable by default — easy to introduce bugs
const start = new Date();
const end = start; // Same reference!
end.setDate(end.getDate() + 7); // Mutates start too

// No timezone support
new Date().toLocaleString('en-US', { timeZone: 'Asia/Ho_Chi_Minh' });
// Works, but fragile — no first-class TZ type
Enter fullscreen mode Exit fullscreen mode

These aren't edge cases. They're production bugs waiting to happen. Every "scheduled for Monday" bug, every "appointment shows wrong time in a different region" complaint traces back to the Date object's fundamental design flaws.

The Temporal Fix: Type-Safe, Immutable, Timezone-Aware

Temporal introduces distinct types for distinct concerns. No more guessing:

Type Use Case
Temporal.Instant A precise UTC moment (like a Unix timestamp)
Temporal.ZonedDateTime A moment + timezone (for scheduling)
Temporal.PlainDate A calendar date (no time, no timezone)
Temporal.PlainTime A wall-clock time (no date, no timezone)
Temporal.PlainDateTime Date + time without timezone info
Temporal.Duration A length of time (e.g., "2 hours 30 minutes")

All Temporal objects are immutable. Operations return new objects. No more mutation surprises.

Getting Started: Install the Polyfill

While Temporal is ES2026 standard, native support in Node.js 24 requires the --harmony-temporal flag (V8 implementation is in progress as of April 2026). For production APIs, use the official polyfill:

npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// In Node.js 24 with --harmony-temporal flag (experimental):
// const { Temporal } = globalThis;

// For production today (polyfill approach):
import { Temporal } from '@js-temporal/polyfill';

// Or CommonJS:
const { Temporal } = require('@js-temporal/polyfill');
Enter fullscreen mode Exit fullscreen mode

Note: Major browsers (Chrome 129+, Firefox 139+, Safari 18.4+) have started shipping native Temporal support as of early 2026. Node.js native support without a flag is expected in Node.js 24 LTS updates later in 2026.

Pattern 1: Storing and Returning Timestamps in REST APIs

The most common mistake: using new Date() and calling .toISOString() without thinking about what you're actually storing.

The wrong way:

// What timezone is this? What format is the client expecting?
app.get('/events/:id', async (req, res) => {
  const event = await db.query('SELECT * FROM events WHERE id = $1', [req.params.id]);
  res.json({
    ...event,
    startsAt: event.starts_at.toISOString(), // Loses timezone info!
  });
});
Enter fullscreen mode Exit fullscreen mode

The Temporal way — explicit and unambiguous:

import { Temporal } from '@js-temporal/polyfill';

app.get('/events/:id', async (req, res) => {
  const event = await db.query('SELECT * FROM events WHERE id = $1', [req.params.id]);

  // Convert DB timestamp (stored as UTC) to Temporal.Instant
  const instant = Temporal.Instant.fromEpochMilliseconds(
    event.starts_at.getTime()
  );

  // Return UTC instant (canonical form for APIs)
  res.json({
    id: event.id,
    title: event.title,
    startsAt: instant.toString(), // "2026-06-15T09:00:00Z" — always UTC, always unambiguous
    timezone: event.timezone,     // Store the original timezone separately
  });
});
Enter fullscreen mode Exit fullscreen mode

Even better — return timezone-aware datetimes:

app.get('/events/:id', async (req, res) => {
  const event = await db.query('SELECT * FROM events WHERE id = $1', [req.params.id]);

  const instant = Temporal.Instant.fromEpochMilliseconds(
    event.starts_at.getTime()
  );

  // Convert to the event's original timezone
  const zdt = instant.toZonedDateTimeISO(event.timezone);

  res.json({
    id: event.id,
    title: event.title,
    startsAt: {
      utc: instant.toString(),
      local: zdt.toString(), // "2026-06-15T16:00:00+07:00[Asia/Ho_Chi_Minh]"
      timezone: event.timezone,
    },
  });
});
Enter fullscreen mode Exit fullscreen mode

This is the pattern used by modern scheduling APIs — always store UTC, always return the original timezone context alongside it.

Pattern 2: Accepting and Validating Date Inputs

Validating date inputs with new Date() is fragile — it silently accepts bad input. Temporal throws on invalid data, making it a natural validation layer.

import { Temporal } from '@js-temporal/polyfill';

function parseEventInput(body) {
  let startDate, endDate;

  try {
    // Strict ISO 8601 parsing — throws on invalid input
    startDate = Temporal.Instant.from(body.startsAt);
  } catch (e) {
    throw new Error(`Invalid startsAt: "${body.startsAt}" is not a valid ISO 8601 timestamp`);
  }

  try {
    endDate = Temporal.Instant.from(body.endsAt);
  } catch (e) {
    throw new Error(`Invalid endsAt: "${body.endsAt}" is not a valid ISO 8601 timestamp`);
  }

  // Validate logical ordering
  if (Temporal.Instant.compare(startDate, endDate) >= 0) {
    throw new Error('endsAt must be after startsAt');
  }

  // Validate minimum duration (e.g., events must be at least 15 minutes)
  const duration = startDate.until(endDate);
  if (duration.total('minutes') < 15) {
    throw new Error('Event must be at least 15 minutes long');
  }

  return { startDate, endDate };
}

app.post('/events', async (req, res) => {
  try {
    const { startDate, endDate } = parseEventInput(req.body);

    // Store as epoch milliseconds in the database
    await db.query(
      'INSERT INTO events (title, starts_at, ends_at) VALUES ($1, $2, $3)',
      [
        req.body.title,
        new Date(startDate.epochMilliseconds),
        new Date(endDate.epochMilliseconds),
      ]
    );

    res.status(201).json({ message: 'Event created' });
  } catch (e) {
    res.status(400).json({ error: e.message });
  }
});
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Multi-Timezone Scheduling Logic

This is where Temporal truly shines. Building a scheduling API that works across timezones is notoriously painful. Here's a clean pattern for "find available slots" in a given user's timezone:

import { Temporal } from '@js-temporal/polyfill';

/**
 * Get the next 7 available booking slots, in the user's local timezone.
 * Business hours: 9 AM - 5 PM, Monday-Friday
 */
function getAvailableSlots(userTimezone, existingBookings = []) {
  const slots = [];
  let current = Temporal.Now.zonedDateTimeISO(userTimezone);

  // Start from the next full hour
  current = current.round({ smallestUnit: 'hour', roundingMode: 'ceil' });

  while (slots.length < 7) {
    const hour = current.hour;
    const dayOfWeek = current.dayOfWeek; // 1=Mon, 7=Sun

    // Skip weekends
    if (dayOfWeek <= 5 && hour >= 9 && hour < 17) {
      const slotEnd = current.add({ hours: 1 });

      // Check if slot is already booked
      const isBooked = existingBookings.some(booking => {
        const bookingStart = Temporal.Instant.from(booking.startsAt)
          .toZonedDateTimeISO(userTimezone);
        return Temporal.ZonedDateTime.compare(bookingStart, current) === 0;
      });

      if (!isBooked) {
        slots.push({
          startsAt: current.toInstant().toString(),
          endsAt: slotEnd.toInstant().toString(),
          localTime: current.toPlainTime().toString(),
          localDate: current.toPlainDate().toString(),
          timezone: userTimezone,
        });
      }
    }

    current = current.add({ hours: 1 });
  }

  return slots;
}

app.get('/slots', async (req, res) => {
  const { timezone = 'UTC' } = req.query;

  try {
    // Validate the timezone
    Temporal.TimeZone.from(timezone); // Throws if invalid
  } catch (e) {
    return res.status(400).json({ error: `Invalid timezone: "${timezone}"` });
  }

  const bookings = await db.query('SELECT * FROM bookings WHERE starts_at > NOW()');
  const slots = getAvailableSlots(timezone, bookings.rows);

  res.json({ timezone, slots });
});
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Duration Calculations for Billing and Rate Limiting

import { Temporal } from '@js-temporal/polyfill';

// API usage tracking — calculate billable time
app.get('/usage/:userId', async (req, res) => {
  const sessions = await db.query(
    'SELECT started_at, ended_at FROM api_sessions WHERE user_id = $1',
    [req.params.userId]
  );

  let totalDuration = new Temporal.Duration();

  for (const session of sessions.rows) {
    const start = Temporal.Instant.fromEpochMilliseconds(session.started_at.getTime());
    const end = Temporal.Instant.fromEpochMilliseconds(session.ended_at.getTime());

    const sessionDuration = start.until(end, { largestUnit: 'hours' });
    totalDuration = totalDuration.add(sessionDuration);
  }

  const normalized = Temporal.Duration.from(totalDuration);

  res.json({
    userId: req.params.userId,
    totalUsage: {
      hours: normalized.hours,
      minutes: normalized.minutes,
      seconds: normalized.seconds,
      totalMinutes: Math.floor(normalized.total('minutes')),
    },
    billableUnits: Math.ceil(normalized.total('minutes') / 15),
  });
});
Enter fullscreen mode Exit fullscreen mode

Pattern 5: Relative Time Without moment.js

import { Temporal } from '@js-temporal/polyfill';

function relativeTime(isoString) {
  const then = Temporal.Instant.from(isoString);
  const now = Temporal.Now.instant();
  const isFuture = Temporal.Instant.compare(then, now) > 0;
  const absDuration = isFuture
    ? now.until(then, { largestUnit: 'years' })
    : then.until(now, { largestUnit: 'years' });

  if (absDuration.years >= 1) return `${absDuration.years}y ${isFuture ? 'from now' : 'ago'}`;
  if (absDuration.months >= 1) return `${absDuration.months}mo ${isFuture ? 'from now' : 'ago'}`;
  if (absDuration.weeks >= 1) return `${absDuration.weeks}w ${isFuture ? 'from now' : 'ago'}`;
  if (absDuration.days >= 1) return `${absDuration.days}d ${isFuture ? 'from now' : 'ago'}`;
  if (absDuration.hours >= 1) return `${absDuration.hours}h ${isFuture ? 'from now' : 'ago'}`;
  if (absDuration.minutes >= 1) return `${absDuration.minutes}m ${isFuture ? 'from now' : 'ago'}`;
  return 'just now';
}
Enter fullscreen mode Exit fullscreen mode

Quick Reference: Date → Temporal Migrations

// Get current time
// Before: new Date()
// After:  Temporal.Now.instant()

// Parse ISO string
// Before: new Date('2026-04-01T09:00:00Z')
// After:  Temporal.Instant.from('2026-04-01T09:00:00Z')

// Add time
// Before: new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000)
// After:  instant.add({ days: 7 })

// Compare dates
// Before: date1 > date2
// After:  Temporal.Instant.compare(instant1, instant2) > 0

// Format for API response
// Before: date.toISOString()
// After:  instant.toString()
Enter fullscreen mode Exit fullscreen mode

Conclusion

The ES2026 Temporal API is the biggest improvement to JavaScript date handling since the language was created. With Stage 4 confirmed on March 11, 2026, and the polyfill production-ready today, there's no reason to wait.

Start with @js-temporal/polyfill. Use Temporal.Instant for UTC storage, Temporal.ZonedDateTime for scheduling logic, and Temporal.Duration for billing and rate limiting. Your future self — and your API consumers — will thank you.


Building APIs? 1xAPI provides developer tools and API infrastructure.

Top comments (0)