npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bjnewman/joi-temporal

v1.0.1

Published

Joi extension for Temporal API types

Readme

joi-temporal

Joi extension for validating and coercing Temporal API types.

npm version CI License: MIT

import Joi from "joi";
import joiTemporal from "@bjnewman/joi-temporal";

const custom = Joi.extend(...joiTemporal);

const schema = custom.object({
    date: custom.plainDate().min("2020-01-01").max("2025-12-31"),
    time: custom.plainTime().min("09:00").max("17:00"),
    duration: custom.duration().positive().max("PT8H"),
});

const { value } = schema.validate({
    date: "2024-03-15",
    time: "14:30",
    duration: "PT2H30M",
});

value.date instanceof Temporal.PlainDate; // true
value.time instanceof Temporal.PlainTime; // true
value.duration instanceof Temporal.Duration; // true

Why not @joi/date or joi-luxon?

Joi's existing date extensions (@joi/date, @reis/joi-luxon, joi-date-dayjs) all coerce to either Date, Luxon DateTime, or Day.js objects — and each comes with trade-offs:

| | @joi/date | @reis/joi-luxon | joi-temporal | |---|---|---|---| | Coerces to | Date | Luxon DateTime | Temporal types | | Timezones | .utc() only | Yes (via Luxon) | Native ZonedDateTime | | Durations | No | No | Temporal.Duration with .positive(), .min(), .max() | | Distinct date vs time | No — everything is Date | No — everything is DateTime | PlainDate, PlainTime, PlainDateTime, Instant, etc. | | Runtime dep | moment (format parsing) | Luxon | None — uses the platform | | Future | moment is deprecated | Luxon is maintained | Temporal is a TC39 standard, shipping in Chrome 137+, Firefox 139+, Node.js 22+ |

The Temporal API is the JavaScript standard that replaces Date. It's already native in major browsers and Node.js, and production-grade polyfills like temporal-polyfill make it usable everywhere else today. Unlike library-specific types, Temporal objects are what the rest of the ecosystem is converging on.

  • Strings in, Temporal objects out — ISO 8601 strings from JSON payloads are coerced to real Temporal instances. No manual parsing.
  • Feels native to Joi — same .min(), .max(), .required(), .messages() chaining you already know.
  • All 8 Temporal types — the right type for each use case instead of stuffing everything into Date.
  • Zero dependencies — just your Joi peer dependency and a Temporal runtime.
  • "now" comparators.min("now") resolves at validation time, not schema construction time.
  • Polyfill now, native later — swap the polyfill for native Temporal support with zero code changes.

Install

npm install @bjnewman/joi-temporal

You'll also need the Temporal API available at runtime. Pick one:

| Environment | How to enable | |---|---| | Node.js 22+ | node --harmony-temporal app.js | | Chrome 137+ / Firefox 139+ | Ships natively | | Everywhere else | npm install temporal-polyfill and import "temporal-polyfill/global" at your entry point |

Peer dependency: joi >= 17.0.0

Supported Types

| Method | Temporal Type | Example Input | |--------|--------------|---------------| | plainDate() | Temporal.PlainDate | "2024-03-15" | | plainTime() | Temporal.PlainTime | "14:30:00" | | plainDateTime() | Temporal.PlainDateTime | "2024-03-15T14:30:00" | | zonedDateTime() | Temporal.ZonedDateTime | "2024-03-15T14:30:00-04:00[America/New_York]" | | instant() | Temporal.Instant | "2024-03-15T14:30:00Z" | | duration() | Temporal.Duration | "PT2H30M" | | plainYearMonth() | Temporal.PlainYearMonth | "2024-03" | | plainMonthDay() | Temporal.PlainMonthDay | "03-15" |

All types coerce from ISO 8601 strings and pass through existing Temporal instances.

API

Comparison Rules

Available on all types except plainMonthDay:

custom.plainDate().min("2020-01-01")   // >= (inclusive)
custom.plainDate().max("2025-12-31")   // <= (inclusive)
custom.plainDate().gt("2020-01-01")    // >  (exclusive)
custom.plainDate().lt("2025-12-31")    // <  (exclusive)
custom.plainDate().gte("2020-01-01")   // alias for .min()
custom.plainDate().lte("2025-12-31")   // alias for .max()

Comparators accept ISO strings or Temporal instances. plainDate, plainDateTime, and plainTime also accept "now".

Duration Rules

custom.duration().positive()   // sign must be > 0
custom.duration().negative()   // sign must be < 0
custom.duration().nonzero()    // sign must not be 0
custom.duration().min("PT1H")  // at least 1 hour
custom.duration().max("P1D")   // at most 1 day

ZonedDateTime Timezone

custom.zonedDateTime().timezone("America/New_York")

Usage with Hapi

ISO strings in JSON payloads are coerced to Temporal objects before your handler runs:

import Hapi from "@hapi/hapi";
import Joi from "joi";
import joiTemporal from "@bjnewman/joi-temporal";

const custom = Joi.extend(...joiTemporal);

server.route({
    method: "POST",
    path: "/bookings",
    options: {
        validate: {
            payload: custom.object({
                date: custom.plainDate().min("now").max("2026-12-31").required(),
                startTime: custom.plainTime().min("09:00").max("17:00").required(),
                duration: custom.duration().positive().min("PT30M").max("PT4H").required(),
            }),
        },
    },
    handler(request) {
        const { date, startTime, duration } = request.payload;
        const end = startTime.add(duration); // already Temporal objects
        return { date: date.toString(), start: startTime.toString(), end: end.toString() };
    },
});

Usage with React Hook Form

Works with @hookform/resolvers and HTML date/time inputs, which produce ISO strings that joi-temporal coerces automatically:

import { useForm } from "react-hook-form";
import { joiResolver } from "@hookform/resolvers/joi";
import Joi from "joi";
import joiTemporal from "@bjnewman/joi-temporal";

const custom = Joi.extend(...joiTemporal);

const schema = custom.object({
    startDate: custom.plainDate().min("now").required()
        .messages({ "temporal.plainDate.min": "Date must be today or later" }),
    meetingTime: custom.plainTime().min("08:00").max("18:00").required()
        .messages({ "temporal.plainTime.min": "Must be during business hours" }),
});

const { register, handleSubmit } = useForm({ resolver: joiResolver(schema) });
// <input type="date" {...register("startDate")} />
// <input type="time" {...register("meetingTime")} />

Error Messages

Every error code can be overridden with .messages():

| Error Code | Default Message | |------------|----------------| | temporal.plainDate.base | "must be a valid ISO 8601 date string or Temporal.PlainDate" | | temporal.plainDate.min | "must be on or after {#limit}" | | temporal.plainDate.max | "must be on or before {#limit}" | | temporal.plainDate.gt | "must be after {#limit}" | | temporal.plainDate.lt | "must be before {#limit}" | | temporal.plainTime.base | "must be a valid ISO 8601 time string or Temporal.PlainTime" | | temporal.plainDateTime.base | "must be a valid ISO 8601 date-time string or Temporal.PlainDateTime" | | temporal.zonedDateTime.base | "must be a valid ISO 8601 date-time string with timezone or Temporal.ZonedDateTime" | | temporal.zonedDateTime.timezone | "must be in timezone {#timezone}" | | temporal.instant.base | "must be a valid ISO 8601 string with offset or Temporal.Instant" | | temporal.duration.base | "must be a valid ISO 8601 duration string or Temporal.Duration" | | temporal.duration.min | "must be at least {#limit}" | | temporal.duration.max | "must be at most {#limit}" | | temporal.duration.positive | "must be a positive duration" | | temporal.duration.negative | "must be a negative duration" | | temporal.duration.nonzero | "must not be zero" | | temporal.plainYearMonth.base | "must be a valid ISO 8601 year-month string or Temporal.PlainYearMonth" | | temporal.plainMonthDay.base | "must be a valid ISO 8601 month-day string or Temporal.PlainMonthDay" |

The {#limit} token is replaced with the ISO string representation of the comparator.

License

MIT