Content Negotiation
What is Content Negotiation?
Content negotiation is the mechanism for serving different representations of a resource based on client preferences.
Accept Header
Client specifies preferred response format:
// Client requests JSON
GET /api/users/123
Accept: application/json
// Client requests XML
GET /api/users/123
Accept: application/xml
// Client accepts multiple formats
GET /api/users/123
Accept: application/json, application/xml;q=0.9Server-Side Implementation
// Node.js/Express - Content negotiation
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.format({
'application/json': () => {
res.json(user);
},
'application/xml': () => {
res.set('Content-Type', 'application/xml');
res.send(convertToXML(user));
},
'text/html': () => {
res.send(`<html><body><h1>${user.name}</h1></body></html>`);
},
'default': () => {
res.status(406).send('Not Acceptable');
}
});
});// .NET - Content negotiation
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var user = await _context.Users.FindAsync(id);
// ASP.NET Core automatically handles content negotiation
// based on Accept header
return Ok(user);
}
// Configure formatters
services.AddControllers(options =>
{
options.RespectBrowserAcceptHeader = true;
options.ReturnHttpNotAcceptable = true;
})
.AddXmlSerializerFormatters();Quality Values (q)
// Client preference with quality values
Accept: application/json;q=1.0, application/xml;q=0.8, text/html;q=0.5
// Server chooses based on quality
app.get('/api/users/:id', (req, res) => {
const user = await User.findById(req.params.id);
// Automatically selects best match
res.format({
'application/json': () => res.json(user),
'application/xml': () => res.send(convertToXML(user)),
'text/html': () => res.send(convertToHTML(user))
});
});Accept-Language
// Language negotiation
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
const lang = req.acceptsLanguages('en', 'es', 'fr') || 'en';
const translations = {
en: { name: 'Name', email: 'Email' },
es: { name: 'Nombre', email: 'Correo' },
fr: { name: 'Nom', email: 'Courriel' }
};
res.json({
...user,
_labels: translations[lang]
});
});Accept-Encoding
// Compression negotiation
const compression = require('compression');
app.use(compression());
// Client request
GET /api/users
Accept-Encoding: gzip, deflate, br
// Server response
HTTP/1.1 200 OK
Content-Encoding: gzipAccept-Charset
// Character set negotiation
app.get('/api/users/:id', (req, res) => {
const user = await User.findById(req.params.id);
const charset = req.acceptsCharsets('utf-8', 'iso-8859-1') || 'utf-8';
res.set('Content-Type', `application/json; charset=${charset}`);
res.json(user);
});Versioning via Content Negotiation
// Version in Accept header
GET /api/users/123
Accept: application/vnd.myapi.v2+json
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
const accept = req.headers.accept;
if (accept.includes('v2')) {
res.json(transformToV2(user));
} else {
res.json(user);
}
});Custom Media Types
// Define custom media types
app.get('/api/users/:id', (req, res) => {
const user = await User.findById(req.params.id);
res.format({
'application/vnd.company.user+json': () => {
res.json({
type: 'user',
version: '1.0',
data: user
});
},
'application/json': () => {
res.json(user);
}
});
});Angular - Setting Accept Header
import { HttpHeaders } from '@angular/common/http';
@Injectable()
export class UserService {
getUserAsJSON(id: string): Observable<User> {
const headers = new HttpHeaders({
'Accept': 'application/json'
});
return this.http.get<User>(`${this.apiUrl}/users/${id}`, { headers });
}
getUserAsXML(id: string): Observable<string> {
const headers = new HttpHeaders({
'Accept': 'application/xml'
});
return this.http.get(`${this.apiUrl}/users/${id}`, {
headers,
responseType: 'text'
});
}
}Proactive vs Reactive Negotiation
// Proactive - Server chooses based on Accept header
app.get('/api/users/:id', (req, res) => {
const user = await User.findById(req.params.id);
res.format({
'application/json': () => res.json(user),
'application/xml': () => res.send(convertToXML(user))
});
});
// Reactive - Client chooses from available options
app.options('/api/users/:id', (req, res) => {
res.json({
formats: [
{ type: 'application/json', url: '/api/users/123.json' },
{ type: 'application/xml', url: '/api/users/123.xml' }
]
});
});Error Handling
// 406 Not Acceptable
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
const accepted = req.accepts(['json', 'xml']);
if (!accepted) {
return res.status(406).json({
error: 'Not Acceptable',
supported: ['application/json', 'application/xml']
});
}
if (accepted === 'json') {
res.json(user);
} else {
res.send(convertToXML(user));
}
});Interview Tips
- Explain content negotiation: Serve different formats
- Show Accept header: Client preferences
- Demonstrate implementation: Node.js, .NET
- Discuss quality values: Preference ordering
- Mention compression: Accept-Encoding
- Show versioning: Via media types
Summary
Content negotiation allows serving different resource representations based on client preferences. Use Accept header for format negotiation, Accept-Language for language, Accept-Encoding for compression. Server selects best match using quality values. Return 406 Not Acceptable if no match. Supports JSON, XML, HTML, and custom formats. Essential for flexible REST APIs.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.