API design · May 9, 2023
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
}
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"
}
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"
}
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"
}
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:
In an API this might look like:
{
"date_of_birth": {
"day": 3,
"month": 7,
"year": 1980
}
}
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
}
}
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
}
}
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"
}
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"
}
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"
}
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"
}
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.