DEV Community

Cover image for Stop Rewriting Your API: How FMiddleware Lets You Deploy to Lambda AND Express with Zero Code Changes
Matthias Steinbauer
Matthias Steinbauer

Posted on

Stop Rewriting Your API: How FMiddleware Lets You Deploy to Lambda AND Express with Zero Code Changes

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 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:

npm install @loupeat/fmiddleware
Enter fullscreen mode Exit fullscreen mode

Define your API handler once:

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);
});
Enter fullscreen mode Exit fullscreen mode

Deploy to Express.js (Local Dev / Docker)

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);
Enter fullscreen mode Exit fullscreen mode

Deploy to AWS Lambda (Production)

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);
};
Enter fullscreen mode Exit fullscreen mode

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:

npm run dev  # Uses Express wrapper
Enter fullscreen mode Exit fullscreen mode

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:

interface CreateNoteRequest {
  title: string;
  content: string;
}

api.post<any, CreateNoteRequest>("/api/notes", async (request) => {
  const { title, content } = request.body; // Fully typed!
  // ...
});
Enter fullscreen mode Exit fullscreen mode

Built-in JSON Schema Validation

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);
Enter fullscreen mode Exit fullscreen mode

Pre-Processors for Authentication

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);
Enter fullscreen mode Exit fullscreen mode

Semantic Error Classes

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`);
}
Enter fullscreen mode Exit fullscreen mode

OpenAPI Generation

Automatically generate OpenAPI 3.0 specs from your handlers:

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"],
});
Enter fullscreen mode Exit fullscreen mode

Real-World Deployment

FMiddleware works with any deployment tool—Serverless Framework, AWS CDK, SST, or plain CloudFormation. Here's an example using Serverless Framework:

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
Enter fullscreen mode Exit fullscreen mode

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

npm install @loupeat/fmiddleware

# For AWS Lambda support
npm install --save-dev @types/aws-lambda
Enter fullscreen mode Exit fullscreen mode

Check out the full documentation and examples:


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!

Top comments (0)