The Golden Rule for API Timestamps
Always return timestamps in ISO 8601 / RFC 3339 format with explicit UTC offset. This means every timestamp in your API response should look like "2024-03-01T14:30:00Z" or "2024-03-01T14:30:00+09:00" — never a bare date string, never an ambiguous local time, never just a Unix integer (unless you document units carefully).
Timestamp Formats Compared
// ❌ Bad: ambiguous local time
{"created_at": "2024-03-01 14:30:00"}
// ❌ Bad: Unix timestamp without documented units
{"created_at": 1709300200} // seconds? milliseconds?
// ✅ Good: RFC 3339 UTC
{"created_at": "2024-03-01T14:30:00Z"}
// ✅ Good: RFC 3339 with explicit offset
{"created_at": "2024-03-01T23:30:00+09:00"}
// ✅ Also acceptable: Unix timestamp with explicit milliseconds key
{"created_at_ms": 1709300200000} // key name clarifies unit
Consistency Across the API
- Pick one format and use it everywhere — don't mix ISO strings in some endpoints and Unix integers in others.
- Use the same field name pattern:
created_at,updated_at,deleted_at— orcreatedAtin camelCase APIs. - Always include timezone info — never return a naked datetime string.
Pagination with Timestamps (Cursor Pagination)
For large datasets, use timestamp-based cursor pagination instead of page numbers:
// Response
{
"data": [...],
"next_cursor": "2024-03-01T14:30:00Z",
"has_more": true
}
// Next request
GET /api/events?before=2024-03-01T14:30:00Z&limit=20
// Server query (PostgreSQL)
-- SELECT * FROM events
-- WHERE created_at < '2024-03-01T14:30:00Z'
-- ORDER BY created_at DESC LIMIT 20
Versioning Timestamp Formats
If you must change timestamp format between API versions, use API versioning and document the change clearly. Never silently change the format — it breaks all existing clients.
GraphQL Scalar Types
# GraphQL — use a custom DateTime scalar
scalar DateTime # Specify: RFC 3339 string in UTC
type Event {
id: ID!
createdAt: DateTime!
scheduledAt: DateTime
}
# Response
{
"createdAt": "2024-03-01T14:30:00Z"
}
Filtering by Date Range
# REST API date range filter — always accept ISO 8601
GET /api/orders?created_after=2024-03-01T00:00:00Z&created_before=2024-03-31T23:59:59Z
# Django — parse incoming timestamps safely
from django.utils.dateparse import parse_datetime
from django.utils import timezone
def get_queryset(self):
after = self.request.query_params.get("created_after")
if after:
dt = parse_datetime(after)
if dt and timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone.utc)
queryset = queryset.filter(created_at__gte=dt)