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/Atom namespace
  • Include required feed elements: id, title, and updated
  • 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 format
  • T — Literal separator
  • 18:30:02 — Time in HH:MM:SS format
  • Z — 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

  1. Timestamps are critical — RFC 3339 format with Z suffix is non-negotiable
  2. Validate before publishing — Invalid feeds break reader integrations
  3. Use Node.js date methods correctlytoISOString() is your friend
  4. Automate validation — Build it into your deployment pipeline
  5. 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.

Recommended Tools

  • VercelDeploy frontend apps instantly with zero config
  • RenderZero-DevOps cloud platform for web apps and APIs