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.
WebApp for unified deployment and access to MooseStack utilities.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=20import 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(); // Middlewareapp.use(logger());app.use(bodyParser()); // Custom error handling middlewareapp.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 middlewareapp.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 checkrouter.get("/health", (ctx) => { ctx.body = { status: "ok", timestamp: new Date().toISOString() };}); // GET with params and queryrouter.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 endpointrouter.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 JWTrouter.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 listenerapp.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" }});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;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 middlewareconst requireAdmin = compose([authMiddleware, adminMiddleware]); router.get("/admin", requireAdmin, async (ctx) => { ctx.body = { message: "Admin access granted" };});import Koa, { Context } from "koa";import { MyTable } from "../tables/MyTable"; // Extend context typeinterface CustomContext extends Context { formatResponse: (data: any) => { success: boolean; data: any };} const app = new Koa<any, CustomContext>(); // Add custom method to contextapp.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);});Koa uses try-catch for error handling:
// Error middlewareapp.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 listenerapp.on("error", (err, ctx) => { console.error("Error:", err);});Organize routes with nested routers:
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 };});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 routersmainRouter.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" }});new WebApp(name, app, config)WebAppConfig:
interface WebAppConfig { mountPath: string; metadata?: { description?: string }; injectMooseUtils?: boolean; // default: true}getMooseUtils(ctx.req)koa-compose for reusable middleware chainsSolution: Use ctx.req not ctx.request:
const moose = getMooseUtils(ctx.req); // Correctconst moose = getMooseUtils(ctx.request); // WrongSolution: Apply middleware in correct order:
app.use(logger()); // 1. Loggingapp.use(bodyParser()); // 2. Body parsingapp.use(errorHandler); // 3. Error handlingapp.use(router.routes()); // 4. RoutesSolution: Import and use correct types:
import { Context, Next } from "koa"; router.get("/path", async (ctx: Context, next: Next) => { // ctx is properly typed});