We value your privacy

This site uses cookies to improve your browsing experience, analyze site traffic, and show personalized content. See our Privacy Policy.

  1. MooseStack
  2. Moose APIs & Web Apps
  3. Koa with MooseStack

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 WebApp for 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 { client, sql } = await getMooseUtils();  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/health
  • GET 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(); // 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 { client, sql } = await getMooseUtils();  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.statement`    SELECT      ${cols.id},      ${cols.event_type},      ${cols.timestamp}    FROM ${UserEvents}    WHERE ${cols.user_id} = ${userId}      ${eventType ? sql.fragment`AND ${cols.event_type} = ${eventType}` : sql.fragment``}    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 { 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 { jwt } = await getMooseUtils();   if (!jwt) {    ctx.throw(401, "Unauthorized");  }   const userId = jwt.sub;  const userRole = jwt.role;   ctx.body = {    message: "Authenticated",    userId,    role: userRole  };}); // Multiple route handlers (middleware chain)const checkAuth = async (ctx: Context, next: Next) => {  const { jwt } = await getMooseUtils();  if (!jwt) {    ctx.throw(401, "Unauthorized");  }  await next();}; router.get("/admin/stats", checkAuth, async (ctx) => {  // jwt is guaranteed to exist here via checkAuth middleware  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"  }});

Accessing Moose Utilities

Call getMooseUtils() with no arguments. It is async, returns MooseUtils directly, and throws on error (no null check needed):

const { client, sql, jwt } = await getMooseUtils();

For row-level security, pass an RLS context:

const { client, sql } = await getMooseUtils({  rlsContext: { org_id: orgId }});

Middleware Patterns

Composition

import compose from "koa-compose"; const authMiddleware = async (ctx: Context, next: Next) => {  const { jwt } = await getMooseUtils();  if (!jwt) {    ctx.throw(401, "Unauthorized");  }  await next();}; const adminMiddleware = async (ctx: Context, next: Next) => {  const { jwt } = await getMooseUtils();  if (!jwt || 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" };});

Custom Context Extensions

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 { client, sql } = await getMooseUtils();  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 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);});

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 { client, sql } = await getMooseUtils();  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 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" }});

WebApp Configuration

new WebApp(name, app, config)

WebAppConfig:

interface WebAppConfig {  mountPath: string;  metadata?: { description?: string };  injectMooseUtils?: boolean; // default: true}

Best Practices

  1. Await getMooseUtils(): Call await getMooseUtils() with no arguments to access Moose utilities
  2. Use ctx.throw(): Koa's built-in error throwing for cleaner code
  3. Leverage async/await: Koa is designed for modern async patterns
  4. Compose middleware: Use koa-compose for reusable middleware chains
  5. Handle errors globally: Use error middleware at the top of middleware stack
  6. Type your context: Extend Context type for custom properties
  7. Organize with routers: Split large applications into nested routers

Troubleshooting

"Moose utilities not available"

Solution: Ensure getMooseUtils() is called inside a request handler within a WebApp-mounted Koa app. The function detects the runtime context automatically and requires no arguments:

const { client, sql } = await getMooseUtils();

Middleware order issues

Solution: Apply middleware in correct order:

app.use(logger());          // 1. Loggingapp.use(bodyParser());      // 2. Body parsingapp.use(errorHandler);      // 3. Error handlingapp.use(router.routes());   // 4. Routes

TypeScript 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});

On this page

Basic ExampleComplete Example with MiddlewareAccessing Moose UtilitiesMiddleware PatternsCompositionCustom Context ExtensionsError HandlingRouter NestingWebApp ConfigurationBest PracticesTroubleshooting"Moose utilities not available"Middleware order issuesTypeScript errors with Context
Edit this page
FiveonefourFiveonefour
Fiveonefour Docs
MooseStackHostingTemplatesGuides
Release Notes
Source563
  • Overview
Build a New App
  • 5 Minute Quickstart
  • Browse Templates
  • Existing ClickHouse
Add to Existing App
  • Next.js
  • Fastify
Fundamentals
  • Moose Runtime
  • MooseDev MCP
  • Language Server
  • Data Modeling
Moose Modules
  • Moose OLAP
  • Moose Streaming
  • Moose Workflows
  • Moose APIs & Web Apps
    • Native APIs
    • Ingest API
    • Analytics API
    • Semantic Layer
    • Workflow Trigger
    • Admin APIs
    • Authentication
    • Use Your Web Framework
    • Overview
    • Express
    • Fastify
    • Koa
    • Raw Node.js
Deployment & Lifecycle
  • Moose Dev
  • Moose Migrate
  • Moose Deploy
Reference
  • API Reference
  • Query Layer
  • Testing Utilities
  • Data Types
  • Table Engines
  • CLI
  • Configuration
  • Observability Metrics
  • Help
  • Release Notes
Contribution
  • Documentation
  • Framework