Skip to content

REST input validation ​

When a client sends a date string in a JSON body, you have three options:

  1. Trust the value and pass it to atemporal().
  2. Validate and throw a 400.
  3. Validate and return a structured error (the atemporal.validate() way).

This recipe shows the third option, which is the most friendly to client developers because it carries an ATEMPORAL_* error code they can match against.

Express ​

ts
import express, { Request, Response } from 'express';
import atemporal from 'atemporal';

const app = express();
app.use(express.json());

app.post('/events', (req: Request, res: Response) => {
  const result = atemporal.validate(req.body.occurredAt);
  if (!result.ok) {
    return res.status(400).json({
      error: 'Invalid `occurredAt`',
      code: result.code,            // 'ATEMPORAL_INVALID_DATE'
      reason: result.reason,
    });
  }
  // result.iso is a valid ISO 8601 string you can store as-is.
  events.push({ occurredAt: result.iso });
  res.status(201).end();
});

Fastify (with JSON Schema) ​

ts
import Fastify from 'fastify';
import atemporal from 'atemporal';

const fastify = Fastify({ logger: true });

const isoDateSchema = {
  type: 'string',
  validate: (value: unknown) => atemporal.validate(value).ok
    ? true
    : atemporal.validate(value).code, // surfaced as a 400
};

fastify.post('/events', {
  schema: {
    body: {
      type: 'object',
      required: ['occurredAt'],
      properties: { occurredAt: isoDateSchema },
    },
  },
}, async (req) => {
  const occurredAt = atemporal(req.body.occurredAt);
  return { stored: occurredAt.format(atemporal.presets.ISO) };
});

Zod ​

Zod does not have a built-in date validator that produces structured errors. We can wrap atemporal.validate():

ts
import { z } from 'zod';
import atemporal from 'atemporal';

const AtemporalDate = z.unknown().transform((value, ctx) => {
  const r = atemporal.validate(value);
  if (!r.ok) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: r.reason ?? 'Invalid date',
      params: { code: r.code },
    });
    return z.NEVER;
  }
  return r.iso!;
});

const Event = z.object({
  occurredAt: AtemporalDate,
});

The params: { code: r.code } field is what your API gateway can use to map errors to localized messages.

Common mistakes to avoid ​

  1. Do not use new Date(req.body.occurredAt) and pass it to atemporal() without validating first. The Date constructor happily accepts new Date('not a date') and returns an Invalid Date object that propagates silently.
  2. Do not store the original string. Always normalize to ISO 8601 (result.iso) before persisting. The original string is a presentation concern, not a storage concern.
  3. Do not use atemporal() and rely on .isValid() downstream. The early-validation pattern above fails fast and gives the client a useful error.

See also ​