Custom Routes
Nile's REST interface is a standard Hono app. After creating your server, you can add custom routes directly on the Hono instance for things Nile doesn't handle through the service/action model, such as webhooks, OAuth callbacks, health checks, or any traditional HTTP endpoint.
Accessing the Hono App
createNileServer returns a NileServer object with a rest.app property. A regular Hono instance:
import { createNileServer } from "@nilejs/nile";
const server = createNileServer({
name: "MyApp",
services: [/* ... */],
rest: {
baseUrl: "/api/v1",
allowedOrigins: ["http://localhost:3000"],
},
});
const app = server.rest?.app;
Adding Routes
Use any Hono method (get, post, put, delete, all) to register custom routes:
Webhook Endpoint
app?.post("/webhooks/stripe", async (c) => {
const signature = c.req.header("stripe-signature");
const body = await c.req.text();
// Verify and process the webhook
const event = verifyStripeSignature(body, signature);
if (!event) {
return c.json({ error: "Invalid signature" }, 401);
}
await processStripeEvent(event);
return c.json({ received: true });
});
OAuth Callback
app?.get("/auth/callback", async (c) => {
const code = c.req.query("code");
const state = c.req.query("state");
if (!code || !state) {
return c.json({ error: "Missing code or state" }, 400);
}
const tokens = await exchangeCodeForTokens(code);
// Set cookie, create session, etc.
return c.redirect("/dashboard");
});
Custom Health Check
app?.get("/health", async (c) => {
const dbHealthy = await checkDatabaseConnection();
const cacheHealthy = await checkCacheConnection();
const healthy = dbHealthy && cacheHealthy;
return c.json({
status: healthy ? "ok" : "degraded",
checks: {
database: dbHealthy ? "up" : "down",
cache: cacheHealthy ? "up" : "down",
},
}, healthy ? 200 : 503);
});
Using Nile Context in Custom Routes
Access the shared NileContext from custom routes using getContext:
import { createNileServer, getContext } from "@nilejs/nile";
const server = createNileServer({
name: "MyApp",
services: [/* ... */],
resources: { database: db, logger },
rest: {
baseUrl: "/api/v1",
allowedOrigins: ["http://localhost:3000"],
},
});
server.rest?.app.post("/webhooks/payment", async (c) => {
const ctx = getContext<typeof db>();
const logger = ctx.resources?.logger;
const database = ctx.resources?.database;
const payload = await c.req.json();
logger?.info({
atFunction: "webhookHandler",
message: "Payment webhook received",
data: { eventType: payload.type },
});
// Use your database, cache, or any shared resource
await database?.insert(payments).values({
eventId: payload.id,
amount: payload.amount,
});
return c.json({ processed: true });
});
Middleware for the Services Endpoint
To add middleware that runs before Nile's built-in POST /services handler, use addMiddleware:
server.rest?.addMiddleware("*", async (c, next) => {
const start = Date.now();
await next();
console.log(`Request took ${Date.now() - start}ms`);
});
// Path-specific middleware
server.rest?.addMiddleware("/api/v1/services", async (c, next) => {
const apiKey = c.req.header("x-api-key");
if (!apiKey) {
return c.json({ error: "API key required" }, 401); // Short-circuit, returns Response, skips handler
}
await next();
});
Middleware registered via addMiddleware runs in registration order, before the POST handler processes the intent. Each middleware receives the Hono context and a next() function to pass control to the next middleware (or the handler). A middleware can return a Response to short-circuit the request. Downstream middleware and the handler will not execute.
Adding Middleware
You can also add Hono middleware to the app for custom routes:
// Apply to specific paths
app?.use("/webhooks/*", async (c, next) => {
const apiKey = c.req.header("x-api-key");
if (apiKey !== process.env.WEBHOOK_SECRET) {
return c.json({ error: "Unauthorized" }, 401);
}
await next();
});
// Then register your webhook handlers
app?.post("/webhooks/stripe", stripeHandler);
app?.post("/webhooks/github", githubHandler);
Route Order
Custom routes registered after createNileServer are added after Nile's built-in routes. The order is:
- CORS middleware
- Rate limiting middleware
- Static file serving (
/assets/*)
- Registered middleware (via
addMiddleware, can short-circuit by returning Response)
POST {baseUrl}/services (Nile RPC)
GET /status (if enabled)
- Your custom routes
- 404 handler
Since Nile's 404 handler catches unmatched routes, register your custom routes before exporting the app for serving.
Full Example
import { createNileServer, getContext } from "@nilejs/nile";
import { Ok } from "slang-ts";
const server = createNileServer({
name: "PaymentService",
services: [
{
name: "payments",
description: "Payment processing",
actions: [
{
name: "list",
description: "List recent payments",
handler: () => Ok({ payments: [] }),
},
],
},
],
resources: { database: db },
rest: {
baseUrl: "/api/v1",
allowedOrigins: ["http://localhost:3000"],
enableStatus: true,
},
});
const app = server.rest?.app;
// Stripe webhook, outside the service/action model
app?.post("/webhooks/stripe", async (c) => {
const ctx = getContext();
const body = await c.req.json();
// Process webhook...
return c.json({ received: true });
});
// OAuth callback
app?.get("/auth/google/callback", async (c) => {
const code = c.req.query("code");
// Exchange code, set session...
return c.redirect("/");
});
export default app;