Building Your First API with Koa
If you know JavaScript and want to build a backend API, Koa is worth a serious look. It’s minimal, modern, and built around async/await from the ground up—which means cleaner code and no callback hell. This Koa API tutorial walks you through the basics: setting up a project, understanding how middleware works, and building a small REST API with GET and POST endpoints.
Key Takeaways
- Koa is a lightweight Node.js framework with no bundled middleware—you compose only what you need.
- Its “onion model” middleware cascade lets each function run logic both before and after downstream handlers.
- The
ctxobject unifies request and response data, keeping route handlers clean and readable. - With
@koa/routerand@koa/bodyparser, you can stand up a working REST API in a single file.
Prerequisites
- Node.js 20+ (current LTS recommended)
- Basic JavaScript knowledge, including
async/await - A terminal and a text editor
What Is Koa and Why Use It?
Koa is a lightweight Node.js web framework created by the team behind Express. Unlike Express, Koa has no bundled middleware—you add only what you need. Its standout feature is the middleware cascade, sometimes called the “onion model,” where each middleware function runs code before and after the next one in the chain.
This makes Koa’s API basics straightforward to reason about: every request flows inward through your middleware stack, and the response flows back outward.
Step 1 — Set Up Your Project
Create a new directory and initialize it:
mkdir koa-items-api && cd koa-items-api
npm init -y
Add "type": "module" to package.json to use ES module syntax, then install dependencies:
npm install koa @koa/router @koa/bodyparser
- koa — the core framework
- @koa/router — official routing middleware
- @koa/bodyparser — parses incoming JSON request bodies
Step 2 — Understand the Koa Middleware API
Every Koa middleware is an async function that receives a ctx (context) object and a next function. The ctx object combines the request and response into one place—ctx.method, ctx.path, ctx.request.body, and ctx.body (the response) all live there.
Calling await next() passes control to the next middleware. Here’s a simple logging example:
app.use(async (ctx, next) => {
const start = Date.now()
await next()
console.log(`${ctx.method} ${ctx.path} — ${Date.now() - start}ms`)
})
Notice that code after await next() runs once the downstream middleware has finished. That’s the onion model in action.
Discover how at OpenReplay.com.
Step 3 — Build a REST API with Koa
Create app.js with a small in-memory items API:
import Koa from "koa"
import Router from "@koa/router"
import bodyParser from "@koa/bodyparser"
const app = new Koa()
const router = new Router()
// In-memory data store
let nextId = 3
let items = [
{ id: 1, name: "Keyboard" },
{ id: 2, name: "Monitor" },
]
// GET /items — return all items
router.get("/items", (ctx) => {
ctx.body = { success: true, data: items }
})
// GET /items/:id — return one item
router.get("/items/:id", (ctx) => {
const item = items.find((i) => i.id === Number(ctx.params.id))
if (!item) {
ctx.status = 404
ctx.body = { error: "Item not found" }
return
}
ctx.body = { success: true, data: item }
})
// POST /items — create a new item
router.post("/items", (ctx) => {
const { name } = ctx.request.body
if (!name) {
ctx.status = 400
ctx.body = { error: "Name is required" }
return
}
const newItem = { id: nextId++, name }
items.push(newItem)
ctx.status = 201
ctx.body = { success: true, data: newItem }
})
app.use(bodyParser())
app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3000, () => console.log("Server running on http://localhost:3000"))
Note: The original
id: items.length + 1approach breaks if you ever delete an item, because the array length shrinks while existing IDs don’t. A separatenextIdcounter avoids duplicate IDs.
Start the server:
node app.js
Test Your Endpoints
Get all items:
curl http://localhost:3000/items
Create an item:
curl -X POST http://localhost:3000/items \
-H "Content-Type: application/json" \
-d '{"name": "Mouse"}'
Expected response:
{ "success": true, "data": { "id": 3, "name": "Mouse" } }
Request a missing item:
curl http://localhost:3000/items/99
{ "error": "Item not found" }
router.allowedMethods() handles unsupported HTTP methods automatically, returning a proper 405 Method Not Allowed response without any extra code.
Conclusion
Koa’s strength is its simplicity. The ctx object keeps request and response handling in one place, and the middleware cascade gives you precise control over how requests flow through your application. This foundation—a Koa server, @koa/router for routing, and @koa/bodyparser for JSON parsing—is all you need to start building a REST API with Koa. From here, you can add a real database, validation, or error-handling middleware one layer at a time.
FAQs
Koa ships with no built-in middleware, so you install only what your project needs. It also uses async/await natively instead of callbacks, which simplifies error handling and control flow. Express relies on a traditional callback-based middleware signature, while Koa's onion model lets each middleware run logic both before and after downstream handlers.
Yes. Koa is stable, well-maintained, and used in production by many teams. Because it is minimal by design, you choose your own router, body parser, and error-handling layers. This gives you full control over your dependency stack and keeps the framework footprint small, which can simplify auditing and long-term maintenance.
Place a try/catch middleware at the top of your middleware stack. Because Koa uses async/await, any error thrown downstream will bubble up to that outer handler. You can then set ctx.status and ctx.body to return a consistent error response. This single middleware replaces the need for repetitive error checks in every route.
Yes. Koa and its official packages such as @koa/router and @koa/bodyparser include TypeScript type definitions. You can set up a standard TypeScript project with ts-node or a build step, import Koa as usual, and get full type safety on the ctx object, route parameters, and request bodies.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.