Express on Lambda
Prerequisites:
- Existing Jaypie monorepo or new project
- AWS CDK knowledge (for deployment)
Overview
Section titled “Overview”This guide walks through creating an Express application using Jaypie patterns that runs on AWS Lambda via serverless-express. The same application works locally with hot reload and deploys to Lambda unchanged.
Project Structure
Section titled “Project Structure”packages/api/├── src/│ ├── app.ts # Express app setup│ ├── handler.ts # Lambda entry point│ ├── routers/│ │ └── v1.router.ts # API router│ └── routes/│ └── health.route.ts # Individual routes├── package.json├── tsconfig.json├── vite.config.ts└── vitest.config.tsStep 1: Create Package
Section titled “Step 1: Create Package”mkdir -p packages/api/src/{routers,routes}Step 2: Package Configuration
Section titled “Step 2: Package Configuration”packages/api/package.json:
{ "name": "@project/api", "version": "0.0.1", "type": "module", "private": true, "main": "dist/handler.js", "types": "dist/handler.d.ts", "exports": { ".": { "import": "./dist/handler.js", "types": "./dist/handler.d.ts" } }, "scripts": { "build": "vite build && tsc --emitDeclarationOnly", "dev": "env-cmd -f .env.local tsx watch src/dev.ts", "test": "vitest run" }}Step 3: Install Dependencies
Section titled “Step 3: Install Dependencies”npm install jaypie @vendia/serverless-express express -w packages/apinpm install -D @jaypie/testkit @types/express vite vitest -w packages/apiStep 4: Express App Setup
Section titled “Step 4: Express App Setup”packages/api/src/app.ts:
import { cors } from "jaypie";import express from "express";
import v1Router from "./routers/v1.router.js";
const app = express();
// Middlewareapp.use(cors());app.use(express.json());
// Routesapp.use("/v1", v1Router);
export default app;Step 5: Lambda Handler
Section titled “Step 5: Lambda Handler”packages/api/src/handler.ts:
import serverlessExpress from "@vendia/serverless-express";
import app from "./app.js";
export const handler = serverlessExpress({ app });Step 6: Router Setup
Section titled “Step 6: Router Setup”packages/api/src/routers/v1.router.ts:
import { Router } from "express";
import healthRoute from "../routes/health.route.js";
const router = Router();
router.get("/health", healthRoute);
export default router;Step 7: Route with Handler
Section titled “Step 7: Route with Handler”packages/api/src/routes/health.route.ts:
import { expressHandler, log } from "jaypie";
export default expressHandler( async (req, res) => { log.trace("[health] responding"); return { status: "healthy" }; }, { name: "health", });Step 8: Local Development Server
Section titled “Step 8: Local Development Server”packages/api/src/dev.ts:
import app from "./app.js";
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`);});packages/api/.env.local:
PROJECT_ENV=localLOG_LEVEL=traceStep 9: Build Configuration
Section titled “Step 9: Build Configuration”packages/api/vite.config.ts:
import { defineConfig } from "vite";import { resolve } from "path";
export default defineConfig({ build: { lib: { entry: resolve(__dirname, "src/handler.ts"), formats: ["es"], fileName: "handler", }, rollupOptions: { external: [ "express", "@vendia/serverless-express", /^jaypie/, /^@jaypie/, ], }, outDir: "dist", emptyOutDir: true, },});packages/api/tsconfig.json:
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "declaration": true, "emitDeclarationOnly": true }, "include": ["src/**/*"]}Step 10: Test Setup
Section titled “Step 10: Test Setup”packages/api/vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({ test: { environment: "node", setupFiles: ["@jaypie/testkit/testSetup"], },});packages/api/src/routes/health.route.spec.ts:
import { describe, expect, it, vi } from "vitest";
vi.mock("jaypie", async () => { const testkit = await import("@jaypie/testkit/mock"); return testkit;});
import healthRoute from "./health.route.js";
describe("Health Route", () => { it("returns healthy status", async () => { const req = {} as any; const res = {} as any; const result = await healthRoute(req, res); expect(result).toEqual({ status: "healthy" }); });});Running Locally
Section titled “Running Locally”npm run dev -w packages/api# Server at http://localhost:8080Building for Lambda
Section titled “Building for Lambda”npm run build -w packages/api# Output in packages/api/dist/handler.jsCDK Deployment
Section titled “CDK Deployment”See CDK Infrastructure for deploying with JaypieExpressLambda.
import { JaypieExpressLambda, JaypieApiGateway } from "@jaypie/constructs";
const api = new JaypieExpressLambda(this, "Api", { code: "../api/dist", handler: "handler.handler",});
new JaypieApiGateway(this, "Gateway", { handler: api, host: "api.example.com",});Adding Routes
Section titled “Adding Routes”With Validation
Section titled “With Validation”import { expressHandler, BadRequestError } from "jaypie";
export default expressHandler( async (req, res) => { const { id } = req.params; const user = await db.users.findById(id); if (!user) throw NotFoundError(); return { data: user }; }, { name: "getUser", validate: [(req) => req.params.id], secrets: ["MONGODB_URI"], });With Setup/Teardown
Section titled “With Setup/Teardown”import { expressHandler } from "jaypie";import { connectMongo, disconnectMongo } from "./db.js";
export default expressHandler( async (req, res) => { return await db.users.find(); }, { setup: [connectMongo], teardown: [disconnectMongo], });Related
Section titled “Related”- Handler Lifecycle - Understanding validate/setup/teardown
- CDK Infrastructure - Deploying to AWS
- @jaypie/express - Express handler reference
- Testing - Testing Express routes