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 processAccept-Language
: Indicates preferred languagesAccept-Encoding
: Defines acceptable content encoding methodsAccept-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:
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
:
# 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:
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:
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:
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:
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
- Always provide defaults - Have a default format in case client preferences can't be met
- Respect the 406 status code - Return 406 (Not Acceptable) when you can't provide any of the requested formats
- Be consistent - Use the same negotiation pattern across your entire API
- Document your supported formats - Make it clear to API consumers what formats you support
- Consider performance - Some formats (like XML) may require more processing time than others
- Use appropriate Content-Type headers - Always set the correct response content type
- 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:
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:
# 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
- Express.js Documentation on res.format()
- MDN Web Docs: Content negotiation
- HTTP specification for content negotiation
Exercises
- Create a simple API that returns user data in JSON, XML, and HTML formats using content negotiation.
- Implement language negotiation for an API that returns greeting messages in at least three different languages.
- Build a product catalog API that supports content negotiation and allows filtering products by category with different response formats.
- Extend the product catalog API to support API versioning through the Accept header.
- 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! :)