REST input validation
When a client sends a date string in a JSON body, you have three options:
- Trust the value and pass it to
atemporal(). - Validate and throw a 400.
- 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
- Do not use
new Date(req.body.occurredAt)and pass it toatemporal()without validating first. TheDateconstructor happily acceptsnew Date('not a date')and returns an Invalid Date object that propagates silently. - 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. - Do not use
atemporal()and rely on.isValid()downstream. The early-validation pattern above fails fast and gives the client a useful error.