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

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:

  • Compare with Express
  • Try Fastify
  • Learn Node.js

Resources: