---
title: "Stop Rewriting Your API: How FMiddleware Lets You Deploy to Lambda AND Express with Zero Code Changes"
published: true
description: "Tired of vendor lock-in? FMiddleware is a framework-agnostic HTTP middleware that lets you write your API once and deploy it anywhere—AWS Lambda, Express.js, or Docker containers."
tags: typescript, node, aws, serverless
cover_image: https://i.imgur.com/Ii8EqT9.jpeg
canonical_url:
---
## The Problem: Serverless Lock-In Is Real
You started with AWS Lambda even used a framework like Serverless.com. It made sense—no servers to manage, pay-per-use pricing, and quick deployments. Life was good.
Then you tried to debug something locally.
The reality is that local development with Lambda is painful. You're constantly dealing with mocked API Gateway events, environment differences between local and deployed code, and debugging workflows that don't match how you'd normally work with Node.js. When something breaks in production, reproducing it locally means wrestling with event structures and Lambda-specific context objects.
And if your team ever decides to move to Docker or Kubernetes? Your entire codebase is tied to Lambda's event structure—migration means rewriting your API layer.
## The Solution: Write Once, Deploy Anywhere
What if you could write your API handlers once and deploy them to *any* runtime without changing a single line of business logic?
That's exactly what [**FMiddleware**](https://github.com/loupeat/fmiddleware) does.
FMiddleware is a framework-agnostic HTTP middleware library that abstracts away the runtime differences between AWS Lambda and Express.js. Your handlers stay the same—only the deployment wrapper changes.
## Quick Start: See It In Action
Install the package:
```bash
npm install @loupeat/fmiddleware
```
Define your API handler **once**:
```typescript
import { FRequest } from "@loupeat/fmiddleware";
interface Note {
id: string;
title: string;
}
// This handler works on BOTH Lambda and Express
api.get("/api/notes", async (request: FRequest<any, any>) => {
const notes: Note[] = [{ id: "1", title: "Hello World" }];
return api.responses.OK<any, Note[]>(request, notes);
});
```
### Deploy to Express.js (Local Dev / Docker)
```typescript
import express from "express";
import { FExpressMiddleware, FRequest } from "@loupeat/fmiddleware";
const app = express();
const api = new FExpressMiddleware();
// Register your routes (same as above)
api.get("/api/notes", async (request: FRequest<any, any>) => {
const notes = [{ id: "1", title: "Hello World" }];
return api.responses.OK(request, notes);
});
// Use FMiddleware as Express middleware
app.use(express.json());
app.all("*", async (req, res) => {
const response = await api.process(req);
res.status(response.statusCode).json(response.body);
});
app.listen(3000);
```
### Deploy to AWS Lambda (Production)
```typescript
import { APIGatewayProxyHandler } from "aws-lambda";
import { FAWSLambdaMiddleware, FRequest } from "@loupeat/fmiddleware";
const api = new FAWSLambdaMiddleware();
// Same exact handler code!
api.get("/api/notes", async (request: FRequest<any, any>) => {
const notes = [{ id: "1", title: "Hello World" }];
return api.responses.OK(request, notes);
});
export const handler: APIGatewayProxyHandler = async (event) => {
return api.process(event);
};
```
**Notice something?** The handler code is identical. Only the wrapper changes.
## Why This Matters for Developer Productivity
### 1. Seamless Local Development
No more mocking Lambda events or fighting with local emulators. Just run your Express server locally and test like any normal Node.js app:
```bash
npm run dev # Uses Express wrapper
```
When you're ready for production, deploy with your Lambda wrapper—zero code changes.
### 2. Easy Migration Path
Already on Lambda and want to move to Docker? Simply:
1. Swap `FAWSLambdaMiddleware` for `FExpressMiddleware`
2. Add a Dockerfile
3. Deploy
Your handlers, validation, authentication—everything stays the same.
### 3. Hybrid Deployments
Running some endpoints on Lambda for cost efficiency and others on ECS for performance? FMiddleware lets you share the exact same handler code across both.
## Batteries Included: Features That Actually Help
FMiddleware isn't just a wrapper—it's a full-featured API toolkit:
### TypeScript-First
Full type safety for requests and responses:
```typescript
interface CreateNoteRequest {
title: string;
content: string;
}
api.post<any, CreateNoteRequest>("/api/notes", async (request) => {
const { title, content } = request.body; // Fully typed!
// ...
});
```
### Built-in JSON Schema Validation
```typescript
const CreateNoteSchema = {
type: "object",
properties: {
title: { type: "string", minLength: 1 },
content: { type: "string" },
},
required: ["title", "content"]
};
api.post("/api/notes", async (request) => {
// request.body is automatically validated
const { title, content } = request.body;
// ...
}, CreateNoteSchema);
```
### Pre-Processors for Authentication
```typescript
const AuthPreProcessor: RequestPreProcessor = {
name: "AuthPreProcessor",
pathPatterns: ["/api/notes/*"],
requestSources: "*", // Works on both Express and Lambda
process: async ({ request }) => {
const authHeader = request.headers["authorization"];
if (!authHeader) {
throw new AuthenticationError("Missing authorization header");
}
const token = authHeader.replace(/Bearer /, "");
const user = await authService.verifyToken(token);
request.context["user"] = user;
}
};
api.addRequestPreProcessor(AuthPreProcessor);
```
### Semantic Error Classes
```typescript
import {
ValidationError, // 400 Bad Request
AuthenticationError, // 401 Unauthorized
ForbiddenError, // 403 Forbidden
NotFoundError, // 404 Not Found
ConflictError, // 409 Conflict
} from "@loupeat/fmiddleware";
// Errors automatically map to correct HTTP status codes
if (!note) {
throw new NotFoundError(`Note ${noteId} not found`);
}
```
### OpenAPI Generation
Automatically generate OpenAPI 3.0 specs from your handlers:
```typescript
import { FExpressMiddleware, OpenAPIMetadata } from "@loupeat/fmiddleware";
api.get("/api/notes", async (request) => {
const notes = await notesService.list();
return api.responses.OK(request, notes);
}, {
summary: "List all notes",
description: "Retrieves all notes for the authenticated user",
tags: ["Notes"],
});
```
## Real-World Deployment
FMiddleware works with any deployment tool—Serverless Framework, AWS CDK, SST, or plain CloudFormation. Here's an example using Serverless Framework:
```yaml
service: notes-api
plugins:
- serverless-esbuild
provider:
name: aws
runtime: nodejs20.x
region: eu-west-1
functions:
api:
handler: src/handler.main
events:
- http:
method: any
path: "api/{proxy+}"
cors: true
timeout: 15
```
## When to Use FMiddleware
FMiddleware is perfect if you:
- Want **flexibility** to switch between serverless and containerized deployments
- Value **local development experience** that mirrors production
- Need a **migration path** away from Lambda without rewriting everything
- Want **type-safe APIs** with built-in validation
- Prefer **framework-agnostic** code that doesn't lock you in
## Get Started Today
```bash
npm install @loupeat/fmiddleware
# For AWS Lambda support
npm install --save-dev @types/aws-lambda
```
Check out the full documentation and examples:
- **GitHub**: [loupeat/fmiddleware](https://github.com/loupeat/fmiddleware)
- **npm**: [@loupeat/fmiddleware](https://www.npmjs.com/package/@loupeat/fmiddleware)
---
Have you dealt with serverless lock-in? How did you handle the migration? I'd love to hear your experiences in the comments!
*FMiddleware is open source and MIT licensed. Contributions welcome!*