본문으로 건너뛰기 Koa.js Complete Guide | Next Generation Node.js Framework

Koa.js Complete Guide | Next Generation Node.js Framework

Koa.js Complete Guide | Next Generation Node.js Framework

이 글의 핵심

Koa.js is a next-generation web framework for Node.js designed by the Express team. It uses async/await for cleaner async code and provides a smaller, more expressive foundation.

Introduction

Koa.js is a new web framework designed by the team behind Express. It aims to be a smaller, more expressive, and more robust foundation for web applications and APIs through async functions.

Express vs Koa

Express (callback-based):

app.get('/users', (req, res, next) => {
  User.find((err, users) => {
    if (err) return next(err);
    res.json(users);
  });
});

Koa (async/await):

router.get('/users', async (ctx) => {
  ctx.body = await User.find();
});

Key Differences:

  • Koa uses async/await (no callbacks)
  • Single ctx object instead of req/res
  • No built-in routing or middleware (smaller core)
  • Better error handling with try/catch

Real-World Adoption

Koa represents the evolution of Node.js web frameworks:

Enterprise Usage:

  • Alibaba: Uses Koa in Egg.js framework for enterprise Node.js applications
  • Tencent: Powers internal services with Koa for its modern async architecture
  • Ant Design Pro: Built on UmiJS which uses Koa for development server
  • ThinkJS: Popular Chinese Node.js framework built on top of Koa

Market Position:

  • 1.2+ million weekly npm downloads (April 2026) - solid adoption among modern Node.js developers
  • 35,000+ GitHub stars - strong community following
  • Created by TJ Holowaychuk - the original creator of Express, bringing years of web framework expertise
  • Used in 100,000+ public GitHub repositories

Why Teams Choose Koa:

  • Modern async/await: Native async patterns instead of callback hell
  • Lightweight core: Only 550 lines of code (Express: 2,500+ lines)
  • Better error handling: async/await enables try/catch for proper error boundaries
  • Clean middleware system: Onion-model middleware with upstream/downstream flow
  • No built-in features: Choose your own routing, body parsing, validation (flexibility)

Framework Evolution:

  • Egg.js: Enterprise-grade Koa framework by Alibaba (10K+ stars)
  • ThinkJS: Full-stack Koa framework with built-in ORM
  • AdonisJS 5+: Uses Koa’s context model internally

When to Choose Koa:

  • ✅ Starting new projects with modern Node.js (v12+)
  • ✅ Team comfortable with async/await
  • ✅ Need minimal framework with maximum flexibility
  • ❌ Need mature ecosystem with thousands of middleware (use Express)
  • ❌ Beginner-friendly framework with conventions (use NestJS)

Koa is perfect for teams that want Express’s simplicity but with modern JavaScript patterns and cleaner async code.

1. Installation

npm install koa @koa/router

Basic Server:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) => {
  ctx.body = 'Hello World';
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

2. Context Object

Koa uses a single ctx (context) object:

app.use(async (ctx) => {
  // Request
  console.log(ctx.method);     // GET
  console.log(ctx.url);         // /users
  console.log(ctx.path);        // /users
  console.log(ctx.query);       // { limit: '10' }
  console.log(ctx.headers);     // { ... }
  console.log(ctx.request.body); // Requires koa-bodyparser
  
  // Response
  ctx.status = 200;
  ctx.body = { message: 'Hello' };
  ctx.set('X-Custom', 'value');
  
  // Helpers
  ctx.throw(400, 'Bad Request');
  ctx.redirect('/new-url');
  ctx.assert(ctx.state.user, 401, 'Unauthorized');
});

3. Middleware

Basic Middleware

const Koa = require('koa');
const app = new Koa();

// Logger middleware
app.use(async (ctx, next) => {
  const start = Date.now();
  await next(); // Call next middleware
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

// Response middleware
app.use(async (ctx) => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Error Handling Middleware

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = {
      error: err.message
    };
    ctx.app.emit('error', err, ctx);
  }
});

// Error event listener
app.on('error', (err, ctx) => {
  console.error('Server error:', err);
});

Common Middleware

npm install koa-bodyparser koa-logger koa-cors koa-helmet
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const logger = require('koa-logger');
const cors = require('@koa/cors');
const helmet = require('koa-helmet');

const app = new Koa();

app.use(helmet());
app.use(cors());
app.use(logger());
app.use(bodyParser());

4. Routing

npm install @koa/router
const Koa = require('koa');
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

// Basic routes
router.get('/', async (ctx) => {
  ctx.body = 'Home';
});

router.get('/users', async (ctx) => {
  ctx.body = [{ id: 1, name: 'John' }];
});

router.post('/users', async (ctx) => {
  const user = ctx.request.body;
  ctx.status = 201;
  ctx.body = user;
});

// Route parameters
router.get('/users/:id', async (ctx) => {
  ctx.body = { id: ctx.params.id };
});

// Query strings
router.get('/search', async (ctx) => {
  const { q, limit = 20 } = ctx.query;
  ctx.body = { query: q, limit };
});

// Use router
app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000);

Nested Routers

// routes/users.js
const Router = require('@koa/router');
const router = new Router({ prefix: '/users' });

router.get('/', async (ctx) => {
  ctx.body = 'Get all users';
});

router.get('/:id', async (ctx) => {
  ctx.body = `Get user ${ctx.params.id}`;
});

module.exports = router;

// app.js
const usersRouter = require('./routes/users');
app.use(usersRouter.routes());

5. REST API Example

const Koa = require('koa');
const Router = require('@koa/router');
const bodyParser = require('koa-bodyparser');

const app = new Koa();
const router = new Router({ prefix: '/api' });

app.use(bodyParser());

let todos = [
  { id: 1, text: 'Learn Koa', done: false },
  { id: 2, text: 'Build API', done: false },
];

// Get all todos
router.get('/todos', async (ctx) => {
  ctx.body = todos;
});

// Get single todo
router.get('/todos/:id', async (ctx) => {
  const todo = todos.find(t => t.id === parseInt(ctx.params.id));
  
  if (!todo) {
    ctx.throw(404, 'Todo not found');
  }
  
  ctx.body = todo;
});

// Create todo
router.post('/todos', async (ctx) => {
  const { text } = ctx.request.body;
  
  ctx.assert(text, 400, 'Text is required');
  
  const todo = {
    id: todos.length + 1,
    text,
    done: false,
  };
  
  todos.push(todo);
  ctx.status = 201;
  ctx.body = todo;
});

// Update todo
router.put('/todos/:id', async (ctx) => {
  const todo = todos.find(t => t.id === parseInt(ctx.params.id));
  
  if (!todo) {
    ctx.throw(404, 'Todo not found');
  }
  
  Object.assign(todo, ctx.request.body);
  ctx.body = todo;
});

// Delete todo
router.delete('/todos/:id', async (ctx) => {
  const index = todos.findIndex(t => t.id === parseInt(ctx.params.id));
  
  if (index === -1) {
    ctx.throw(404, 'Todo not found');
  }
  
  todos.splice(index, 1);
  ctx.status = 204;
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000);

6. Authentication

npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

const JWT_SECRET = 'your-secret-key';

// Register
router.post('/auth/register', async (ctx) => {
  const { email, password } = ctx.request.body;
  
  const hashedPassword = await bcrypt.hash(password, 10);
  
  // Save user to database...
  const user = { id: 1, email, password: hashedPassword };
  
  ctx.status = 201;
  ctx.body = { message: 'User created' };
});

// Login
router.post('/auth/login', async (ctx) => {
  const { email, password } = ctx.request.body;
  
  // Find user in database...
  const user = { id: 1, email, password: '$2a$10$...' };
  
  const isValid = await bcrypt.compare(password, user.password);
  
  ctx.assert(isValid, 401, 'Invalid credentials');
  
  const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '7d' });
  
  ctx.body = { token };
});

// Auth middleware
const authenticate = async (ctx, next) => {
  const token = ctx.headers.authorization?.replace('Bearer ', '');
  
  ctx.assert(token, 401, 'Unauthorized');
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    ctx.state.userId = decoded.userId;
    await next();
  } catch (error) {
    ctx.throw(401, 'Invalid token');
  }
};

// Protected route
router.get('/profile', authenticate, async (ctx) => {
  ctx.body = { userId: ctx.state.userId };
});

7. File Upload

npm install @koa/multer multer
const multer = require('@koa/multer');

const upload = multer({ dest: 'uploads/' });

// Single file
router.post('/upload', upload.single('file'), async (ctx) => {
  ctx.body = { file: ctx.file };
});

// Multiple files
router.post('/upload-multiple', upload.array('files', 5), async (ctx) => {
  ctx.body = { files: ctx.files };
});

8. Database Integration

With Mongoose

npm install mongoose
const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/myapp');

const User = mongoose.model('User', {
  name: String,
  email: String,
});

router.get('/users', async (ctx) => {
  ctx.body = await User.find();
});

router.post('/users', async (ctx) => {
  const user = new User(ctx.request.body);
  await user.save();
  ctx.status = 201;
  ctx.body = user;
});

With Prisma

npm install @prisma/client
npx prisma init
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

router.get('/users', async (ctx) => {
  ctx.body = await prisma.user.findMany();
});

router.post('/users', async (ctx) => {
  ctx.body = await prisma.user.create({
    data: ctx.request.body
  });
});

9. Validation

npm install koa-validate
const validate = require('koa-validate');
validate(app);

router.post('/users', async (ctx) => {
  ctx.checkBody('email').notEmpty().isEmail();
  ctx.checkBody('password').notEmpty().len(6, 20);
  
  if (ctx.errors) {
    ctx.status = 400;
    ctx.body = { errors: ctx.errors };
    return;
  }
  
  // Create user...
});

10. Best Practices

1. Error Handling

// Global error handler
app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = {
      status: 'error',
      message: err.message
    };
    
    // Log error
    if (ctx.status === 500) {
      console.error(err);
    }
  }
});

// Custom error class
class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
  }
}

// Usage
router.get('/users/:id', async (ctx) => {
  const user = await User.findById(ctx.params.id);
  
  if (!user) {
    throw new AppError('User not found', 404);
  }
  
  ctx.body = user;
});

2. Environment Variables

require('dotenv').config();

const PORT = process.env.PORT || 3000;
const DATABASE_URL = process.env.DATABASE_URL;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

3. Graceful Shutdown

const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM signal received: closing HTTP server');
  server.close(() => {
    console.log('HTTP server closed');
    // Close database connections
    process.exit(0);
  });
});

11. Testing

npm install --save-dev jest supertest
const request = require('supertest');
const app = require('./app');

describe('GET /api/users', () => {
  it('should return all users', async () => {
    const response = await request(app.callback())
      .get('/api/users')
      .expect(200);
    
    expect(Array.isArray(response.body)).toBe(true);
  });
});

Summary

Koa.js is the modern alternative to Express:

  • Async/await for clean async code
  • Context object instead of req/res
  • Smaller core with opt-in features
  • Better error handling with try/catch
  • Elegant middleware composition

Key Takeaways:

  1. Use ctx for request and response
  2. Async/await for all middleware
  3. ctx.throw() for errors
  4. @koa/router for routing
  5. Smaller core means more control

Next Steps:

Resources:


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Complete Koa.js guide for modern Node.js applications.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Koa.js, Node.js, JavaScript, Backend, REST API, Async.