Koa with MooseStack
Mount Koa applications within your MooseStack project using the WebApp class. Koa is an expressive, minimalist framework by the Express team, designed for modern async/await patterns.
Choose your integration path
- Already running Koa outside MooseStack? Keep it separate and call MooseStack data with the client SDK. The Querying Data guide has TypeScript examples.
- Want to mount Koa in your MooseStack project? Continue below with
WebAppfor unified deployment and access to MooseStack utilities.
Basic Example
app/apis/koaApp.ts
import Koa from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import { WebApp, getMooseUtils } from "@514labs/moose-lib";
import { MyTable } from "../tables/MyTable";
const app = new Koa();
const router = new Router();
app.use(bodyParser());
router.get("/health", (ctx) => {
ctx.body = { status: "ok" };
});
router.get("/data", async (ctx) => {
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.status = 500;
ctx.body = { error: "Moose utilities not available" };
return;
}
const { client, sql } = moose;
const limit = parseInt((ctx.query.limit as string) || "10");
try {
const query = sql`
SELECT
${MyTable.columns.id},
${MyTable.columns.name},
${MyTable.columns.createdAt}
FROM ${MyTable}
ORDER BY ${MyTable.columns.createdAt} DESC
LIMIT ${limit}
`;
const result = await client.query.execute(query);
ctx.body = await result.json();
} catch (error) {
ctx.status = 500;
ctx.body = { error: String(error) };
}
});
app.use(router.routes());
app.use(router.allowedMethods());
export const koaApp = new WebApp("koaApp", app, {
mountPath: "/koa",
metadata: { description: "Koa API" }
});Access your API:
GET http://localhost:4000/koa/healthGET http://localhost:4000/koa/data?limit=20
Complete Example with Middleware
app/apis/advancedKoaApp.ts
import Koa, { Context, Next } from "koa";
import Router from "@koa/router";
import bodyParser from "koa-bodyparser";
import logger from "koa-logger";
import { WebApp, getMooseUtils } from "@514labs/moose-lib";
import { UserEvents } from "../tables/UserEvents";
import { UserProfile } from "../tables/UserProfile";
const app = new Koa();
const router = new Router();
// Middleware
app.use(logger());
app.use(bodyParser());
// Custom error handling middleware
app.use(async (ctx: Context, next: Next) => {
try {
await next();
} catch (error) {
ctx.status = error.status || 500;
ctx.body = {
error: error.message || "Internal Server Error"
};
ctx.app.emit("error", error, ctx);
}
});
// Custom logging middleware
app.use(async (ctx: Context, next: Next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// Health check
router.get("/health", (ctx) => {
ctx.body = {
status: "ok",
timestamp: new Date().toISOString()
};
});
// GET with params and query
router.get("/users/:userId/events", async (ctx) => {
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.throw(500, "Moose utilities not available");
}
const { client, sql } = moose;
const { userId } = ctx.params;
const limit = parseInt((ctx.query.limit as string) || "10");
const eventType = ctx.query.eventType as string;
const cols = UserEvents.columns;
const query = sql`
SELECT
${cols.id},
${cols.event_type},
${cols.timestamp}
FROM ${UserEvents}
WHERE ${cols.user_id} = ${userId}
${eventType ? sql`AND ${cols.event_type} = ${eventType}` : sql``}
ORDER BY ${cols.timestamp} DESC
LIMIT ${limit}
`;
const result = await client.query.execute(query);
const events = await result.json();
ctx.body = {
userId,
count: events.length,
events
};
});
// POST endpoint
router.post("/users/:userId/events", async (ctx) => {
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.throw(500, "Moose utilities not available");
}
const { userId } = ctx.params;
const { eventType, data } = ctx.request.body as any;
// Validation
if (!eventType || !data) {
ctx.throw(400, "eventType and data are required");
}
// Handle POST logic
ctx.body = {
success: true,
userId,
eventType,
data
};
ctx.status = 201;
});
// Protected route with JWT
router.get("/protected", async (ctx) => {
const moose = getMooseUtils(ctx.req);
if (!moose?.jwt) {
ctx.throw(401, "Unauthorized");
}
const userId = moose.jwt.sub;
const userRole = moose.jwt.role;
ctx.body = {
message: "Authenticated",
userId,
role: userRole
};
});
// Multiple route handlers (middleware chain)
const checkAuth = async (ctx: Context, next: Next) => {
const moose = getMooseUtils(ctx.req);
if (!moose?.jwt) {
ctx.throw(401, "Unauthorized");
}
await next();
};
router.get("/admin/stats", checkAuth, async (ctx) => {
const moose = getMooseUtils(ctx.req);
// moose.jwt is guaranteed to exist here
ctx.body = { stats: "admin stats" };
});
app.use(router.routes());
app.use(router.allowedMethods());
// Error listener
app.on("error", (err, ctx) => {
console.error("Server error:", err);
});
export const advancedKoaApp = new WebApp("advancedKoa", app, {
mountPath: "/api/v1",
metadata: {
description: "Advanced Koa API with middleware chain"
}
});Accessing Moose Utilities
Use ctx.req to access the underlying Node.js request:
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.throw(500, "Utilities not available");
}
const { client, sql, jwt } = moose;Middleware Patterns
Composition
import compose from "koa-compose";
const authMiddleware = async (ctx: Context, next: Next) => {
const moose = getMooseUtils(ctx.req);
if (!moose?.jwt) {
ctx.throw(401, "Unauthorized");
}
await next();
};
const adminMiddleware = async (ctx: Context, next: Next) => {
const moose = getMooseUtils(ctx.req);
if (!moose?.jwt || moose.jwt.role !== "admin") {
ctx.throw(403, "Forbidden");
}
await next();
};
// Compose middleware
const requireAdmin = compose([authMiddleware, adminMiddleware]);
router.get("/admin", requireAdmin, async (ctx) => {
ctx.body = { message: "Admin access granted" };
});Custom Context Extensions
import Koa, { Context } from "koa";
import { MyTable } from "../tables/MyTable";
// Extend context type
interface CustomContext extends Context {
formatResponse: (data: any) => { success: boolean; data: any };
}
const app = new Koa<any, CustomContext>();
// Add custom method to context
app.context.formatResponse = function(data: any) {
return {
success: true,
timestamp: new Date().toISOString(),
data
};
};
router.get("/data", async (ctx: CustomContext) => {
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.throw(500, "Utilities not available");
}
const { client, sql } = moose;
const result = await client.query.execute(sql`
SELECT
${MyTable.columns.id},
${MyTable.columns.name},
${MyTable.columns.status}
FROM ${MyTable}
WHERE ${MyTable.columns.status} = 'active'
LIMIT 10
`);
const data = await result.json();
ctx.body = ctx.formatResponse(data);
});Error Handling
Koa uses try-catch for error handling:
// Error middleware
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
// Custom error handling
ctx.status = err.statusCode || err.status || 500;
ctx.body = {
error: {
message: err.message,
status: ctx.status
}
};
// Emit error event
ctx.app.emit("error", err, ctx);
}
});
// Error listener
app.on("error", (err, ctx) => {
console.error("Error:", err);
});Router Nesting
Organize routes with nested routers:
app/apis/routers/usersRouter.ts
import Router from "@koa/router";
import { getMooseUtils } from "@514labs/moose-lib";
export const usersRouter = new Router({ prefix: "/users" });
usersRouter.get("/:userId", async (ctx) => {
const moose = getMooseUtils(ctx.req);
if (!moose) {
ctx.throw(500, "Utilities not available");
}
const { userId } = ctx.params;
// Query logic
ctx.body = { userId };
});app/apis/mainApp.ts
import Koa from "koa";
import Router from "@koa/router";
import { WebApp } from "@514labs/moose-lib";
import { usersRouter } from "./routers/usersRouter";
const app = new Koa();
const mainRouter = new Router();
// Nest routers
mainRouter.use("/api", usersRouter.routes(), usersRouter.allowedMethods());
app.use(mainRouter.routes());
app.use(mainRouter.allowedMethods());
export const mainApp = new WebApp("mainApp", app, {
mountPath: "/v1",
metadata: { description: "Main API with nested routers" }
});WebApp Configuration
new WebApp(name, app, config)WebAppConfig:
interface WebAppConfig {
mountPath: string;
metadata?: { description?: string };
injectMooseUtils?: boolean; // default: true
}Best Practices
- Use ctx.req for utilities: Access Moose utilities via
getMooseUtils(ctx.req) - Use ctx.throw(): Koa’s built-in error throwing for cleaner code
- Leverage async/await: Koa is designed for modern async patterns
- Compose middleware: Use
koa-composefor reusable middleware chains - Handle errors globally: Use error middleware at the top of middleware stack
- Type your context: Extend Context type for custom properties
- Organize with routers: Split large applications into nested routers
Troubleshooting
”Moose utilities not available”
Solution: Use ctx.req not ctx.request:
const moose = getMooseUtils(ctx.req); // Correct
const moose = getMooseUtils(ctx.request); // WrongMiddleware order issues
Solution: Apply middleware in correct order:
app.use(logger()); // 1. Logging
app.use(bodyParser()); // 2. Body parsing
app.use(errorHandler); // 3. Error handling
app.use(router.routes()); // 4. RoutesTypeScript errors with Context
Solution: Import and use correct types:
import { Context, Next } from "koa";
router.get("/path", async (ctx: Context, next: Next) => {
// ctx is properly typed
});