Skip to main content

Express Content Negotiation

Introduction

Content negotiation is a powerful HTTP feature that allows a server to serve different versions or representations of the same resource based on what the client prefers or can handle. In Express.js applications, content negotiation enables your API to respond with data in various formats like JSON, XML, HTML, or plain text depending on what the client requests.

When building modern web applications, it's important to make your APIs flexible enough to cater to different client needs. Content negotiation is especially useful when your API serves diverse clients such as web browsers, mobile applications, or other services that might each prefer different data formats.

Understanding Content Negotiation

Content negotiation works through request headers sent by clients. The most common headers for negotiation are:

  • Accept: Specifies which content types the client can process
  • Accept-Language: Indicates preferred languages
  • Accept-Encoding: Defines acceptable content encoding methods
  • Accept-Charset: Specifies preferred character sets

Express provides built-in methods to handle these headers and respond accordingly.

Basic Content Type Negotiation

Using res.format()

Express's res.format() method is the cornerstone of content negotiation. It takes an object with keys corresponding to MIME types and executes the handler for the best match based on the request's Accept header.

Here's a basic example:

javascript
app.get('/api/user/:id', (req, res) => {
const user = {
id: req.params.id,
name: "John Doe",
email: "[email protected]"
};

res.format({
'text/html': () => {
res.send(`
<html>
<body>
<h1>${user.name}</h1>
<p>Email: ${user.email}</p>
</body>
</html>
`);
},

'application/json': () => {
res.json(user);
},

'text/plain': () => {
res.send(`User: ${user.name}\nEmail: ${user.email}`);
},

'default': () => {
// Default response when no formats match
res.status(406).send('Not Acceptable');
}
});
});

When a client makes a request with an Accept header, Express automatically responds with the appropriate format:

  • Browser request with Accept: text/html will receive HTML
  • API client with Accept: application/json will receive JSON
  • Terminal tool with Accept: text/plain will receive plain text

Testing Content Negotiation

You can test this with tools like curl:

bash
# Request JSON
curl -H "Accept: application/json" http://localhost:3000/api/user/123
# Output: {"id":"123","name":"John Doe","email":"[email protected]"}

# Request HTML
curl -H "Accept: text/html" http://localhost:3000/api/user/123
# Output: <html><body><h1>John Doe</h1><p>Email: [email protected]</p></body></html>

# Request plain text
curl -H "Accept: text/plain" http://localhost:3000/api/user/123
# Output: User: John Doe
# Email: [email protected]

Advanced Content Negotiation

Negotiating with Different Extensions

You can implement URL-based negotiation by supporting different extensions:

javascript
app.get('/api/data.:format?', (req, res) => {
const data = {
name: "Sample data",
value: 42
};

// Get format from URL extension or default to JSON
const format = req.params.format || 'json';

switch(format) {
case 'json':
res.json(data);
break;
case 'xml':
const xmlData = `
<data>
<name>${data.name}</name>
<value>${data.value}</value>
</data>
`;
res.type('application/xml').send(xmlData);
break;
case 'txt':
res.type('text/plain').send(`Name: ${data.name}\nValue: ${data.value}`);
break;
default:
res.status(400).send('Unsupported format');
}
});

With this implementation, clients can request data in different formats simply by changing the URL:

  • /api/data.json returns JSON
  • /api/data.xml returns XML
  • /api/data.txt returns plain text

Setting Default Formats

It's good practice to provide a default response format:

javascript
app.use((req, res, next) => {
// If no specific Accept header is provided, default to JSON
if (!req.get('Accept') || req.get('Accept') === '*/*') {
req.headers.accept = 'application/json';
}
next();
});

Language Negotiation

You can also negotiate content based on language preferences:

javascript
const translations = {
'en': {
greeting: 'Hello',
welcome: 'Welcome to our service'
},
'es': {
greeting: 'Hola',
welcome: 'Bienvenido a nuestro servicio'
},
'fr': {
greeting: 'Bonjour',
welcome: 'Bienvenue à notre service'
}
};

app.get('/greet', (req, res) => {
// Use the accept-language module for more robust language negotiation
const acceptLanguage = require('accept-language');
acceptLanguage.languages(['en', 'es', 'fr']);

const language = acceptLanguage.get(req.headers['accept-language']) || 'en';
const messages = translations[language];

res.format({
'application/json': () => {
res.json(messages);
},
'text/html': () => {
res.send(`
<html>
<body>
<h1>${messages.greeting}</h1>
<p>${messages.welcome}</p>
</body>
</html>
`);
},
'default': () => {
res.json(messages);
}
});
});

Real-World Application: RESTful API with Content Negotiation

Let's build a more complete example of a RESTful API that supports content negotiation:

javascript
const express = require('express');
const app = express();
const xmlparser = require('express-xml-bodyparser');
const xml2js = require('xml2js');

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(xmlparser());

// In-memory product database
const products = [
{ id: 1, name: 'Laptop', price: 999.99 },
{ id: 2, name: 'Smartphone', price: 699.99 },
{ id: 3, name: 'Headphones', price: 199.99 }
];

// Helper for XML conversion
function objectToXml(obj) {
const builder = new xml2js.Builder();
return builder.buildObject({ data: obj });
}

// GET all products with content negotiation
app.get('/api/products', (req, res) => {
res.format({
'application/json': () => {
res.json(products);
},
'application/xml': () => {
const xml = objectToXml(products);
res.type('application/xml').send(xml);
},
'text/html': () => {
let html = '<html><body><h1>Products</h1><ul>';
products.forEach(product => {
html += `<li>${product.name}: $${product.price}</li>`;
});
html += '</ul></body></html>';
res.send(html);
},
'text/csv': () => {
let csv = 'id,name,price\n';
products.forEach(product => {
csv += `${product.id},${product.name},${product.price}\n`;
});
res.type('text/csv').send(csv);
},
'default': () => {
res.status(406).send('Not Acceptable');
}
});
});

// GET a single product with content negotiation
app.get('/api/products/:id', (req, res) => {
const product = products.find(p => p.id === parseInt(req.params.id));

if (!product) {
return res.status(404).send('Product not found');
}

res.format({
'application/json': () => {
res.json(product);
},
'application/xml': () => {
const xml = objectToXml(product);
res.type('application/xml').send(xml);
},
'text/html': () => {
const html = `
<html>
<body>
<h1>${product.name}</h1>
<p>Price: $${product.price}</p>
<p>ID: ${product.id}</p>
</body>
</html>
`;
res.send(html);
},
'default': () => {
res.status(406).send('Not Acceptable');
}
});
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

This API can:

  • Return product data in JSON, XML, HTML, or CSV formats
  • Accept client requests for specific formats via the Accept header
  • Return appropriate HTTP status codes for unsupported formats

Best Practices for Content Negotiation

  1. Always provide defaults - Have a default format in case client preferences can't be met
  2. Respect the 406 status code - Return 406 (Not Acceptable) when you can't provide any of the requested formats
  3. Be consistent - Use the same negotiation pattern across your entire API
  4. Document your supported formats - Make it clear to API consumers what formats you support
  5. Consider performance - Some formats (like XML) may require more processing time than others
  6. Use appropriate Content-Type headers - Always set the correct response content type
  7. Test with different clients - Ensure your API works with browsers, API tools, mobile apps, etc.

Content Negotiation and API Versioning

Content negotiation can also be used for API versioning:

javascript
app.get('/api/users', (req, res) => {
// Get API version from Accept header
// Example: Accept: application/vnd.myapi.v2+json
const acceptHeader = req.get('Accept') || '';
const match = acceptHeader.match(/application\/vnd\.myapi\.v(\d+)\+json/);
const version = match ? parseInt(match[1]) : 1; // Default to version 1

let users;

if (version === 1) {
// v1 just returns basic user info
users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
} else if (version === 2) {
// v2 includes more detailed information
users = [
{ id: 1, name: 'Alice', email: '[email protected]', role: 'admin' },
{ id: 2, name: 'Bob', email: '[email protected]', role: 'user' }
];
}

res.json(users);
});

Clients can request specific API versions:

bash
# Request API v1
curl -H "Accept: application/vnd.myapi.v1+json" http://localhost:3000/api/users

# Request API v2
curl -H "Accept: application/vnd.myapi.v2+json" http://localhost:3000/api/users

Summary

Content negotiation is a powerful feature in Express.js that allows your applications to serve different representations of the same resource based on client preferences. By implementing content negotiation, your APIs become more versatile and can better serve a diverse range of clients.

We've covered:

  • Basic content negotiation with res.format()
  • Advanced negotiation techniques including URL-based negotiation
  • Supporting multiple data formats (JSON, XML, HTML, CSV)
  • Language negotiation
  • Best practices for implementing content negotiation
  • Using content negotiation for API versioning

Incorporating content negotiation into your Express applications makes them more flexible, future-proof, and client-friendly.

Additional Resources

Exercises

  1. Create a simple API that returns user data in JSON, XML, and HTML formats using content negotiation.
  2. Implement language negotiation for an API that returns greeting messages in at least three different languages.
  3. Build a product catalog API that supports content negotiation and allows filtering products by category with different response formats.
  4. Extend the product catalog API to support API versioning through the Accept header.
  5. Create middleware that logs the requested content type and the actual content type provided in the response.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)