Разработка бэкенда сайта на Node.js (Koa)
Koa — минималистичный фреймворк от создателей Express, переосмысленный под async/await. Там где Express требует next() и колбеки, Koa работает через async/await и middleware-стек, который выполняется по принципу «луковицы»: запрос проходит middleware сверху вниз, потом ответ — снизу вверх.
Выбирают Koa тогда, когда нужна полная свобода выбора библиотек без мнений фреймворка, но с нормальной обработкой async-кода в отличие от Express.
Как работает middleware
import Koa from 'koa'
import Router from '@koa/router'
const app = new Koa()
// Middleware логирования — оборачивает всё ниже
app.use(async (ctx, next) => {
const start = Date.now()
await next() // выполняет всё остальное
const ms = Date.now() - start
console.log(`${ctx.method} ${ctx.url} - ${ctx.status} - ${ms}ms`)
})
// Error handling
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = err.statusCode || err.status || 500
ctx.body = {
error: process.env.NODE_ENV === 'production' ? 'Internal Server Error' : err.message
}
ctx.app.emit('error', err, ctx)
}
})
Это принципиальное отличие от Express: в Koa после await next() вы возвращаетесь обратно в middleware с доступом к финальному состоянию ответа. В Express это невозможно без хаков.
Роутинг
@koa/router — официальный роутер:
import Router from '@koa/router'
import bodyParser from '@koa/bodyparser'
const router = new Router({ prefix: '/api/v1' })
// Middleware для конкретного роута
router.get('/products',
authenticate,
async (ctx) => {
const { page = 1, limit = 20 } = ctx.query
const offset = (page - 1) * limit
const [items, total] = await Promise.all([
db.query('SELECT * FROM products LIMIT $1 OFFSET $2', [limit, offset]),
db.query('SELECT COUNT(*) FROM products')
])
ctx.body = {
data: items.rows,
pagination: {
page: Number(page),
limit: Number(limit),
total: Number(total.rows[0].count)
}
}
}
)
router.post('/products', authenticate, requireRole('admin'), async (ctx) => {
const data = ctx.request.body
// ctx.request.body доступен через @koa/bodyparser
const product = await ProductService.create(data)
ctx.status = 201
ctx.body = product
})
app
.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods()) // автоматически обрабатывает OPTIONS и 405
Структура проекта
src/
index.js # точка входа
app.js # создание koa-приложения
middleware/
auth.js
errorHandler.js
requestLogger.js
validate.js # валидация через zod или joi
routes/
index.js # агрегирует роутеры
products.js
users.js
orders.js
services/
products.js
users.js
models/ # или repositories/
config/
utils/
Валидация через Zod
Koa не включает валидацию — подключаем Zod:
import { z } from 'zod'
const createProductSchema = z.object({
name: z.string().min(2).max(255),
price: z.number().positive(),
categoryId: z.number().int().positive(),
description: z.string().optional(),
attributes: z.record(z.unknown()).optional()
})
// Middleware-фабрика для валидации body
const validateBody = (schema) => async (ctx, next) => {
const result = schema.safeParse(ctx.request.body)
if (!result.success) {
ctx.status = 422
ctx.body = { errors: result.error.flatten().fieldErrors }
return
}
ctx.validatedBody = result.data
await next()
}
router.post('/products',
authenticate,
validateBody(createProductSchema),
async (ctx) => {
const product = await ProductService.create(ctx.validatedBody)
ctx.status = 201
ctx.body = product
}
)
Session и аутентификация
JWT через koa-jwt или вручную через jsonwebtoken:
import jwt from 'jsonwebtoken'
const authenticate = async (ctx, next) => {
const authHeader = ctx.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
ctx.throw(401, 'No token provided')
}
try {
const token = authHeader.slice(7)
ctx.state.user = jwt.verify(token, process.env.JWT_SECRET)
await next()
} catch {
ctx.throw(401, 'Invalid or expired token')
}
}
Сессии через koa-session + Redis store:
import session from 'koa-session'
import RedisStore from 'koa-redis'
app.keys = [process.env.SESSION_SECRET]
app.use(session({
store: RedisStore({ client: redisClient }),
maxAge: 86400000 * 7, // 7 дней
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}, app))
Загрузка файлов
@koa/multer для multipart:
import multer from '@koa/multer'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
fileFilter: (req, file, cb) => {
if (!file.mimetype.startsWith('image/')) {
return cb(new Error('Only images allowed'))
}
cb(null, true)
}
})
router.post('/upload',
authenticate,
upload.single('file'),
async (ctx) => {
const file = ctx.file
const key = `uploads/${Date.now()}-${file.originalname}`
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype
}))
ctx.body = { url: `https://${process.env.CDN_HOST}/${key}` }
}
)
Когда Koa не подходит
Koa — хорошая база, но требует самостоятельной сборки: нет встроенной валидации, нет swagger-генерации, нет DI. Если проект растёт и нужна структура — лучше Fastify (производительность + схемы) или NestJS (архитектура). Koa остаётся актуальным для небольших API, прокси-серверов и проектов, где команда хочет полный контроль без фреймворк-магии.
Сроки разработки
- Проектирование + базовый стек: роуты, middleware, подключение БД — 3–5 дней
- Бизнес-логика CRUD + аутентификация — 1–2 недели
- Интеграции (email, файлы, платёжки) — 1–2 недели по необходимости
- Тестирование (jest + supertest) — 3–5 дней
Простой API для сайта-визитки, лендинга или небольшого корпоративного сайта: 3–6 недель. Koa быстро стартует, но требует аккуратности в организации кода — без архитектурных соглашений проект быстро превращается в «Express-спагетти нового поколения».







