Serve JSON or Other Formats Using ctx.accepts() and ctx.is()
Content Negotiation
Use Koa's built-in ctx.accepts() and ctx.is() helpers to inspect Accept and Content-Type headers and respond with the right format or a 406 Not Acceptable.
What you'll learn
- Inspect the Accept header with ctx.accepts() to choose a response format
- Check the request Content-Type with ctx.is() before parsing
- Return 406 when the server cannot satisfy the client's Accept header
HTTP content negotiation lets a single endpoint respond in multiple formats
depending on what the client declares it can accept. The client sends an
Accept header; the server inspects it and picks the best match.
ctx.accepts()
ctx.accepts() reads the Accept request header and returns the best matching
type from the list you provide, or false if none match.
router.get("/articles/:id", async (ctx) => {
const article = await ArticleService.findById(ctx.params.id);
const type = ctx.accepts("json", "text/plain");
if (type === "json") {
ctx.type = "application/json";
ctx.body = article;
} else if (type === "text/plain") {
ctx.type = "text/plain";
ctx.body = `${article.title}\n\n${article.body}`;
} else {
ctx.throw(406, "Not Acceptable — supported: application/json, text/plain");
}
}); When the client sends Accept: application/json (or omits the header entirely),
Koa returns "json". If the client sends Accept: application/xml and you do
not list XML, ctx.accepts() returns false and you should respond 406.
ctx.is()
ctx.is() checks the Content-Type of the incoming request body — useful
for POST/PUT routes that may receive either JSON or form data:
router.post("/articles", async (ctx) => {
if (!ctx.is("application/json")) {
ctx.throw(415, "Unsupported Media Type — send application/json");
}
const { title, body } = ctx.request.body;
ctx.status = 201;
ctx.body = await ArticleService.create({ title, body });
}); ctx.is() accepts mime-type strings and wildcards ("json" matches
application/json; "*/json" matches any +json variant).
Setting the Response Type
Always set ctx.type explicitly when returning non-JSON so Koa sends the correct
Content-Type response header:
ctx.type = "application/json"; // default for plain objects
ctx.type = "text/csv";
ctx.type = "application/xml"; Negotiation in Practice
| Accept header | ctx.accepts(“json”,“text”) | Status |
|---|---|---|
| application/json | ”json” | 200 |
| text/plain | ”text” | 200 |
| (omitted) | “json” | 200 — first type wins |
| application/xml | false | 406 |
Up Next
Versioning an API lets you evolve the contract without breaking existing clients.
API Versioning →