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
ctxobject instead ofreq/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:
- Use
ctxfor request and response - Async/await for all middleware
ctx.throw()for errors- @koa/router for routing
- Smaller core means more control
Next Steps:
- Compare with Express
- Try Fastify
- Learn Node.js
Resources: