How to validate Atom feeds with RFC 3339 timestamps on Node.js 2025
How to Validate Atom Feeds with RFC 3339 Timestamps on Node.js (2025)
Atom feeds power content syndication across millions of websites, but developers often struggle with validation—especially when dealing with timestamp formatting. If you're building a feed reader, aggregator, or content management system in Node.js, you need to understand how Atom feed validation works, particularly around RFC 3339 compliance.
This guide walks you through validating Atom feeds programmatically, catching timestamp errors before they break your readers, and implementing proper validation in your Node.js applications.
What Makes an Atom Feed Valid?
Atom is an XML-based syndication format standardized by the IETF. Every Atom feed must:
- Be well-formed XML
- Use the
http://www.w3.org/2005/Atomnamespace - Include required feed elements:
id,title, andupdated - Use RFC 3339-compliant timestamps for all date values
- Use plain text for element values (no HTML entity encoding)
The most common validation failure? Incorrect timestamp formatting. Developers frequently use ISO 8601 strings without the Z timezone indicator or fail to include milliseconds when required.
Understanding RFC 3339 Requirements
RFC 3339 is a strict timestamp standard. Your Atom feed's <updated> element and any entry timestamps must follow this exact format:
2003-12-13T18:30:02Z
Breakdown:
2003-12-13— Date in YYYY-MM-DD formatT— Literal separator18:30:02— Time in HH:MM:SS formatZ— UTC timezone indicator (required)
Invalid formats that fail validation:
2003-12-13 18:30:02(space instead of T)2003-12-13T18:30:02(missing Z)2003-12-13T18:30:02+00:00(RFC 3339 allows this, but Atom prefers Z)2003-12-13T18:30:02.123(missing timezone)
Building an Atom Feed Validator in Node.js
Let's create a reusable validator that checks both structure and timestamp compliance:
const xml2js = require('xml2js');
const parser = new xml2js.Parser();
const RFC3339_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/;
class AtomValidator {
async validateFeed(xmlString) {
const errors = [];
const warnings = [];
try {
const feed = await parser.parseStringPromise(xmlString);
const atomFeed = feed.feed;
// Check required elements
if (!atomFeed.id || atomFeed.id.length === 0) {
errors.push('Missing required element: <id>');
}
if (!atomFeed.title || atomFeed.title.length === 0) {
errors.push('Missing required element: <title>');
}
if (!atomFeed.updated || atomFeed.updated.length === 0) {
errors.push('Missing required element: <updated>');
}
// Validate feed-level timestamp
if (atomFeed.updated) {
const timestamp = atomFeed.updated[0];
if (!RFC3339_REGEX.test(timestamp)) {
errors.push(
`Invalid RFC 3339 timestamp in <feed><updated>: "${timestamp}". ` +
`Expected format: 2003-12-13T18:30:02Z`
);
}
}
// Validate author (recommended)
const feedHasAuthor = atomFeed.author && atomFeed.author.length > 0;
const entriesHaveAuthor = atomFeed.entry &&
atomFeed.entry.every(entry => entry.author && entry.author.length > 0);
if (!feedHasAuthor && !entriesHaveAuthor) {
warnings.push(
'Feed should contain at least one <author> element unless all entries have authors'
);
}
// Validate entries
if (atomFeed.entry) {
atomFeed.entry.forEach((entry, index) => {
if (!entry.id || entry.id.length === 0) {
errors.push(`Entry ${index + 1}: Missing required element <id>`);
}
if (!entry.updated || entry.updated.length === 0) {
errors.push(`Entry ${index + 1}: Missing required element <updated>`);
}
if (entry.updated) {
const timestamp = entry.updated[0];
if (!RFC3339_REGEX.test(timestamp)) {
errors.push(
`Entry ${index + 1}: Invalid RFC 3339 timestamp: "${timestamp}". ` +
`Expected format: 2003-12-13T18:30:02Z`
);
}
}
});
}
return {
valid: errors.length === 0,
errors,
warnings,
entriesCount: atomFeed.entry ? atomFeed.entry.length : 0
};
} catch (err) {
return {
valid: false,
errors: [`XML Parse Error: ${err.message}`],
warnings: []
};
}
}
}
module.exports = AtomValidator;
Validating Your Atom Feed
Here's how to use the validator in your application:
const AtomValidator = require('./atomValidator');
const fs = require('fs');
const validator = new AtomValidator();
const feedXml = fs.readFileSync('./feed.xml', 'utf-8');
validator.validateFeed(feedXml).then(result => {
console.log('Valid:', result.valid);
console.log('Entries:', result.entriesCount);
if (result.errors.length > 0) {
console.error('Errors:');
result.errors.forEach(err => console.error(` - ${err}`));
}
if (result.warnings.length > 0) {
console.warn('Warnings:');
result.warnings.forEach(warn => console.warn(` - ${warn}`));
}
});
Common Validation Errors and Fixes
| Error | Cause | Fix |
|-------|-------|-----|
| Missing required element: <id> | Feed element missing | Add unique URN: <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> |
| Invalid RFC 3339 timestamp | Wrong format (e.g., missing Z) | Use .toISOString() in Node.js, then ensure Z suffix: new Date().toISOString() |
| Missing timezone indicator | Timestamp without Z or offset | Append Z for UTC: 2023-12-13T18:30:02Z |
| Missing <updated> in entry | Entry lacks modification timestamp | Add: <updated>2023-12-13T18:30:02Z</updated> |
| No author at feed or entry level | Neither feed nor entries have authors | Add at least one: <author><name>Author Name</name></author> |
Generating RFC 3339 Timestamps Correctly
When building your Atom feed dynamically, always generate timestamps in proper format:
// ✅ Correct
const timestamp = new Date().toISOString();
// Output: "2023-12-13T18:30:02.123Z" (includes milliseconds)
// ✅ Also correct (without milliseconds)
const timestamp = new Date().toISOString().split('.')[0] + 'Z';
// Output: "2023-12-13T18:30:02Z"
// ❌ Wrong - missing Z
const timestamp = new Date().toISOString().split('.')[0];
// Output: "2023-12-13T18:30:02" (INVALID)
// ❌ Wrong - using local timezone
const date = new Date();
const timestamp = date.toLocaleString(); // "12/13/2023, 1:30:02 PM" (INVALID)
Integration with Feed Aggregators
Before publishing your Atom feed, validate it programmatically in your build process or middleware:
const express = require('express');
const app = express();
const AtomValidator = require('./atomValidator');
const validator = new AtomValidator();
app.get('/feed.xml', async (req, res) => {
const feedXml = generateAtomFeed(); // Your feed generation logic
const validation = await validator.validateFeed(feedXml);
if (!validation.valid) {
return res.status(400).json({
error: 'Feed validation failed',
details: validation.errors
});
}
res.set('Content-Type', 'application/atom+xml; charset=utf-8');
res.send(feedXml);
});
Testing Your Implementation
Always test with actual feed readers. Many aggregators silently skip invalid feeds. Use:
- W3C Feed Validator (https://validator.w3.org/feed/)
- RSS readers like Feedly or Inoreader (test for silent failures)
- Your own validator before deployment
Key Takeaways
- Timestamps are critical — RFC 3339 format with Z suffix is non-negotiable
- Validate before publishing — Invalid feeds break reader integrations
- Use Node.js date methods correctly —
toISOString()is your friend - Automate validation — Build it into your deployment pipeline
- Test with real aggregators — Not all validators catch all errors
By implementing this validation approach, you'll ensure your Atom feeds integrate seamlessly with feed readers and aggregators across the web.