Que formato usar para timestamps em JSON
Resumo: Números de época e strings ISO têm cada um seus trade-offs, mas misturá-los é o que realmente quebra as coisas. Use ISO 8601 com offset, armazene UTC, renderize no fuso do usuário.

Resumo da ópera: use ISO 8601 com offset explícito no fio, armazene UTC com um Z final e converta apenas na hora de exibir; se preferir números de época, fixe segundos versus milissegundos globalmente e coloque a unidade no nome do campo.
Por que 2026-10-30 14:30:00 não basta
Essa string não carrega fuso. 14:30 em Xangai e 14:30 em Londres estão a oito horas de distância, e não há como saber qual é o pretendido. new Date('2026-10-30 14:30:00') analisa no fuso local do runtime, então o servidor e o navegador derivam dois instantes diferentes de um mesmo payload — o tipo de bug que nunca se reproduz.
Uma 2026-10-30 nua não é um instante de forma alguma. Bom para aniversários e períodos de cobrança, errado para uma hora de criação.
Dois formatos viáveis
Strings ISO 8601
{
"createdAt": "2026-10-30T14:30:00+08:00",
"updatedAt": "2026-10-30T06:30:00Z"
}
Legível, ordenável quando o formato é consistente, precisão de milissegundos se necessário e autodescritivo.
Números de época
{ "createdAt": 1793519400 }
Compactos e inequívocos, mas ilegíveis, e segundos versus milissegundos é um perigo permanente. Se escolher épocas, padronize a unidade e coloque-a no nome do campo: createdAtMs é bem mais seguro que createdAt.
Convenções que vale fixar cedo
| Item | Convenção |
|---|---|
| Formato no fio | ISO 8601 com Z ou +08:00 |
| Zona de armazenamento | UTC, para que o horário de verão nunca entre na história |
| Precisão | Milissegundos bastam |
| Nomeação de campo | createdAt, updatedAt, expiresAt |
| Datas puras | Um campo date separado, por exemplo birthday: "1990-01-01" |
| Durações | Inteiro mais uma unidade, por exemplo timeoutMs: 3000 |
Renderização
new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'Europe/London',
}).format(new Date('2026-10-30T06:30:00Z'));
Especifique timeZone explicitamente quando a exibição não deve seguir o dispositivo. Contar com o padrão do navegador é correto para horas voltadas ao usuário e errado para relatórios.
Três bugs recorrentes
Datas nuas são analisadas como meia-noite UTC. De acordo com a especificação ECMAScript, strings somente data são UTC enquanto strings de data-hora são locais. Essa inconsistência é uma fonte confiável de bugs de um dia a mais ou a menos.
A zona do banco de dados discorda do serializador. datetime do MySQL não tem zona; grava local, lê como UTC, e tudo desloca pelo offset.
Horário de verão. Armazenar horas locais futuras cria instantes que não existem ou ocorrem duas vezes. UTC não tem essas lacunas.
Perguntas frequentes
Como devo escolher entre ISO 8601 e números de época?
Use ISO 8601 quando pessoas leem o payload, quando você quer ordem de classificação lexical ou quando o valor precisa carregar a própria zona; o custo é cerca de uma dúzia de bytes extras e uma biblioteca de análise. Use números de época quando a compacidade e o tratamento trivial entre linguagens importam mais, mas fixe segundos versus milissegundos globalmente e diga qual unidade o campo guarda.
Uma data nua e um timestamp devem compartilhar o mesmo campo?
Não. Uma 2026-10-30 nua é um dia de calendário sem hora nem fuso, enquanto um campo de timestamp deve carregar um instante exato. Mantenha aniversários e períodos de cobrança em um campo date separado e use um campo At em ISO 8601 para horas de criação ou atualização.