DEV Community

James Moschou
James Moschou

Posted on • Originally published at criteria.sh

REST API date format best practices

Date and time information is so common in APIs that they can make or break your API's developer experience.

Most people have an intuitive concept of dates and times, based on their culture, educational background and life experience. Unfortunately, this natural intuition can bias people towards designing poor APIs that don't scale across use cases, geographies or end users.

Different use cases require different approaches

There is a lot of overly simplistic advice within engineering practice, such as "always normalize dates to UTC" or "always use "ISO 8601". This is good advice for many use cases, but not all. Product management can help engineering understand the API's specific use case in detail so that the team can make informed decisions on how to design the API together.

Use cases for date and time information fields usually fall under two broad categories: system dates and dates used by humans. The use case that your field falls into informs how you design it.

System dates are generated on the server

System dates mark the precise time certain events took place within the system. System dates are also commonly referred to as "timestamps". Two ubiquitous examples are the times a record was created and modified.

System dates enable use cases such as sorting a list of records chronologically or auditing the system accurately.

When it comes to system dates, millisecond-level precision and unambiguity are must-have requirements. Being able to make before and after comparisons are more important than being able to describe, say, which day of the week the event happened on.

To summarise the characteristics of system dates:

  • Generated by the system
  • Millisecond-level precision
  • Unambiguous interpretation

Dates used by humans are interpreted within a context

Dates used by humans typically represent real-world events and are usually entered into the system by the user. Examples include dates of birth, arrival and departure times for flights, a recurring time to turn on your home's heating, and so on.

Unlike system dates, they will almost never need to be specified to the millisecond. In fact, they may not have a time at all. It all depends on the situation.

It is impossible to separate a date entered by a user from the user's cultural context. This cultural context will include the user's language, geographic location, and community. The user's language informs how to represent date and time information as text. The geographic location informs within which timezone to interpret a point in time. The user's community determines which calendar system is used to track years, months and days.

To summarise the characteristics of dates used by humans:

  • Specified by the user
  • Different levels of precision, depending on the use case
  • Interpreted according to the user's cultural context

Formatting system dates in APIs

JSON does not have a native date type, so APIs typically represent dates as either numbers or strings. This leads to a number of trade-offs. For system dates, the developer experience should be optimized for precision and correctness.

Approach 1: Use a numerical value to represent the elapsed time since an epoch date

System dates imply a scientific, linear view of time. Most programming languages internally represent dates as the number of seconds that have elapsed since that have elapsed since 00:00:00 UTC on 1 January 1970. This is known as Unix time and is a sensible choice for representing a date as a number.

You can see what today's date as a Unix timestamp looks like using unixtimestamp.com.

Timestamps in an API might look like this:

{
  "created_at": 1682648103
}
Enter fullscreen mode Exit fullscreen mode

Advantages of this approach:

  • The value is opaque to the developer, discouraging them from performing error-prone date arithmetic.
  • Widespread support across programming languages to parse and serialize values.

A downside to this approach is that it is not readable by humans. Does 1682648103 refer to a date in the future or the past? This will make it hard for developers to debug and troubleshoot requests and responses that use this format.

Approach 2: Use the display string format

If we want to provide the developer using the API with more readability than a Unix timestamp, we can format the date as a string. A very naive approach would be to format dates the same way they would be displayed to the user.

This might look like:

{
  "created_at": "8/4/2023, 11:51:16 am"
}
Enter fullscreen mode Exit fullscreen mode

You could describe this as the "quick and dirty" approach. But it is a bad approach for APIs:

  • It's ambiguous whether 8/4/2023 refers to the 8th of April or the 4th of August.
  • Not all users will expect the same date format depending on their locale.
  • It's unclear within what time zone the time should be interpreted.
  • A developer using this API would probably find this format unfamiliar.
  • System libraries probably lack out-of-the-box ways to parse and serialize dates in this format, placing more work on the developer.

This approach is mentioned for demonstration purposes, but do not design your API this way.

Approach 3: Use an internationally recognized standard

Fortunately, there is already an internationally adopted standard for formatting dates and times as strings: ISO 8601.

A timestamp formatted using ISO 8601 looks like this:

{
  "created_at": "2023-04-28T01:52:25Z"
}
Enter fullscreen mode Exit fullscreen mode

Advantages of this format include:

  • No ambiguity between day-month or month-day ordering.
  • The timezone offset can be included or the value can be explicitly denoted as UTC.
  • It's readable by humans, aiding in development and troubleshooting.
  • Most language standard libraries provide ways to parse and serialize dates in this format.
  • Can easily be converted to the user's locale on the client.
  • Readable by developers during development or debugging.

The above example ends with the "Z" character, indicating that the value is in UTC time. Time zones can be hard for developers to reason about, so consider only returning values in UTC time so that different values can be easily compared with each other.

An alternative to the ISO 8601 standards is RFC 3339. The ISO 8601 standard allows for multiple variations in the format, whereas RFC 3339 is sometimes described as a "profile" of ISO 8601 that is more strict. In most scenarios, these standards are interchangeable and the differences are too nuanced to go into here.

Formatting dates used by humans in APIs

Computing has been around for less than a century, yet humans have been describing dates and times for millennia. So it's no surprise that trying to convert human dates to a computer representation is complicated!

When designing APIs to communicate dates, you need to consider how much of the cultural context is known upfront by the server, how much is specific to each client, and to what degree your API need to be able to convert information between different contexts.

Converting between different contexts, or even just supporting multiple contexts, might require you to keep and communicate a normalized representation through your API.

Approach 1: Use reduced precision formats of ISO 8601 where possible

The ISO 8601 standards allows for lower levels precision than what is needed for timestamps.

For example, this is what a date-only field would look like in an API:

{
  "membership_expiry": "2023-09-24"
}
Enter fullscreen mode Exit fullscreen mode

Approach 2: Encode each date component as a separate field

Depending on the use case it may make more sense to design the API to match the user experience.

An example of this would be formatting date of birth fields as objects with separate subfields for day, month and year. This matches common UX patterns:

Date of birth field

In an API this might look like:

{
  "date_of_birth": {
    "day": 3,
    "month": 7,
    "year": 1980
  }
}
Enter fullscreen mode Exit fullscreen mode

This API design is assuming that all users have western concepts of birth dates and use the Gregorian calendar.

If this is not a fair assumption for your application, an advantage of the object format is that you can include a discriminator field, to inform how the date should be interpreted.

{
  "date_of_birth": {
    "calendar": "gregorian",
    "day": 3,
    "month": 7,
    "year": 1980
  }
}
Enter fullscreen mode Exit fullscreen mode

As an example of a different cultural context, Thailand uses the Thai solar calendar, in which the current year at 543 years ahead of the Gregorian calendar. Furthermore, many people only have their year of birth listed on official records.

The date_of_birth field may look like this, with month and day becoming optional fields when calendar is equal to "buddhist".

{
  "date_of_birth": {
    "calendar": "buddhist",
    "year": 2523
  }
}
Enter fullscreen mode Exit fullscreen mode

Document how to interpret dates that can be interpreted in multiple ways

Since non-system dates are often less precise than system dates, they can introduce subjectivity into your business logic that should clarify for your API's consumers.

For example, if a user's membership expires on 7 May 2023, does that mean it is still valid throughout the day on 7 May, or did it already expire the night before at 12:00 AM? In which timezone did it expire — the user's timezone or the system's timezone?

Regardless of what decisions your business logic makes, you should include those decisions in your API's documentation.

Should you include the timezone in the API?

Generally, interfaces don't display timezone information as it would be repetitive and cluttered. Nonetheless, the APIs powering such interfaces do need to consider timezone information in order for times to be displayed accurately.

Approach 1: Always normalize dates as UTC

Since end users don't consume APIs directly, it can make sense to transmit dates as UTC and rely on the application to convert them into the appropriate timezone when displaying them.

This is advantageous because the application is often better positioned to determine what the most appropriate time zone is. For example, it could be the user's timezone determined by the operating system, or it could be a user preference owned by the application. Many B2B apps even have a shared preference for the entire account so that dashboards and reports present the same information to all users.

The following API has a purchased_at field containing a UTC date, which the paplication can transform into an appropriate representation for display as needed.

{
  "purchased_at": "2023-03-24T11:00:10Z"
}
Enter fullscreen mode Exit fullscreen mode

Approach 2: Include the timezone as a separate field

Normalizing dates to UTC works well for dates in the past. However, for scheduled events in the future, UTC is not a good fit since timezones and timezone offsets can change in the meantime and the resulting date may not be what the user expected.

Having a separate field for timezone also makes sense where it is important to display to the user. Examples include a calendar event for a virtual meeting where the attendees live in different countries or a flight itinerary where the arrival and departure airports are in different cities.

An API with a separate field for timezone may look like this:

{
  "departure_time": "2023-09-24T11:00:00",
  "departure_timezone": "Australia/Sydney"
}
Enter fullscreen mode Exit fullscreen mode

It's best practice to format timezones as values from the IANA timezone database instead of reinventing the wheel. For example, Australia/Sydney. This ensures widespread compatibility with different systems.

Approach 3: Encode the timezone offset with the date field

If "Australia/Sydney" refers to a timezone, the timezone offset would be Australian Eastern Standard Time (AEST), or UTC+10:00. Alternatively, it may be Australian Daylight Saving Time (AEDT), which is UTC+11:00.

Since the timezone offset can change throughout the year for a given timezone, it is not appropriate to have a timezone offset as a standalone field. However, there are certain situations where it makes sense to encode the offset as part of the date field.

An example might be an album of sunset photos taken around the world. A user would expect each photo to be taken around the same time of day, i.e. dusk. It would be strange if some photos were displayed at 2 am due to timezone conversion. Here it doesn't make sense to normalize all photo timestamps to UTC, or even a single timezone. Yet it is also important to convey the precise time each photo was taken.

{
  "taken_at": "2023-01-020T18:30:01-05:00"
}
Enter fullscreen mode Exit fullscreen mode

This format allows both the local time to be easily obtained and the absolute point in time to be calculated.

Approach 4: Separate fields for local time and UTC time

Often it can be simpler for the developer to have multiple fields available to them, for each specific use case:

{
    "taken_at_utc": "2023-01-020T18:30:01-05:00"
    "taken_at_local": "2023-01-020T23:30:01Z"
}
Enter fullscreen mode Exit fullscreen mode

Naming date fields

In the above examples timestamp fields all end with the suffix _at. This is a good convention to adopt as it provides an additional signifier to the developer that the value of the field should be interpreted as a timestamp.

Other examples that do not follow this naming convention are not timestamps, and so need to be parsed differently.

Summary

Hopefully, this article illustrates that good design is always dependent on the use case.

That being said, there are some general principles that can be followed as a shortcut:

  • System dates should be formatted using the ISO 8601 standards, be in UTC time, and have the _at suffix.
  • Other dates should use the less precise formats from ISO 8601 unless the use case would benefit from a more specialized format.
  • Consider the timezone of the event and whether it needs to be communicated separately through the API.
  • Avoid assuming the user's cultural context. Instead, design APIs to be as inclusive as possible.

The best-designed APIs will be the result of a collaboration between product and engineering. A tool like Criteria can facilitate deep collaboration amongst a cross-functional team to produce world-class APIs.

Top comments (3)

Collapse
 
alexpgmr profile image
Alex

Hello, thank you, great article!

1.

{
  "membership_expiry": "2023-09-24"
}
Enter fullscreen mode Exit fullscreen mode

2.

{
  "membership_expiry": "2023-09"
}
Enter fullscreen mode Exit fullscreen mode

Can we use just year and month (See: 2)?
Wouldn't that violate the ISO 8601 standard?

Collapse
 
jcmosc profile image
James Moschou

Hey Alex,

ISO 8601 standardizes a lot more types of date and time information than a full-precision timestamp. e.g. time intervals, durations, and even lower precision dates like this.

You can even specify week of the year like 2023-W18

cl.cam.ac.uk/~mgk25/iso-time.html

Whether programming languages and standards libraries support parsing these formats is a completely different question, and the answer is probably that they don't. So you'll need to consider this in the overall developer experience of the API.

In my opinion, it's better for the API to reflect the product's intent, rather than obscure semantics to fit a particular programming language. You can remedy gaps in DX with SDKs or link to guides with sample code from the field-level documentation.

Collapse
 
alexpgmr profile image
Alex

Thank you very much for the detailed answer!