Skip to content

Mike-Medvedev/meebo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

meebo_logo

The missing API validation library for Express

npm version TypeScript License: ISC

InstallationUsageOpenAPIConfiguration


Why Meebo?

Express is the most popular Node.js framework, but it was built before TypeScript existed...

Existing solutions require too much:

  • tRPC: Amazing, but requires you to abandon REST...
  • ts-rest: Powerful, but requires defining contracts separately...
  • tsoa: Generates code from decorators, heavy setup...

Meebo takes a different approach: Keep your Express routes exactly as they are and simply add your schema


The Problem

const router = express.Router();

router.post("/users", (req, res) => {
  const user = req.body; // req.body is type any and not validated at runtime
  res.json({ user }); // res.json returns anything and is not validated either
});

The Solution

import { TypedRouter, swagger } from "meebo";
import { z } from "zod";

const router = TypedRouter(express.Router());

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

router.post("/users", { request: UserSchema, response: UserSchema }, (req, res) => {
  req.body; // typed + validated using UserSchema ✅
  res.json({ name: "John", email: "john@example.com" }); // typed + validated using UserSchema ✅
});

You now have an API where

  • TypeScript knows the exact shape of req.body, req.query, req.params, and your response
  • Full autocomplete and intellisense on your requests, responses, queries, params, and headers
  • Zod validates everything at runtime, ensuring data matches and gives very helpful errors
  • Swagger UI and OpenAPI json generated from all your endpoints and schema

Install

npm install meebo zod express
npm install -D typescript @types/express @types/node

Requirements: TypeScript, Express 5+, Zod 3 or 4

Usage

// Validate request body, response, query, params, headers
router.get(
  "/users/:id",
  {
    params: z.object({ id: z.string() }),
    query: z.object({ limit: z.coerce.number().optional() }),
    response: UserSchema,
  },
  (req, res) => {
    req.params.id; // string
    req.query.limit; // number | undefined
  },
);

OpenAPI / Swagger

Add swagger() as the last middleware to serve swagger ui docs at /docs:

const app = express();
app.use("/api", router);
app.use(swagger("My API"));

Authorize button (Bearer token)

To show an "Authorize"so you can enter a Bearer token and test protected routes from the docs, pass { bearerAuth: true } as the second argument:

app.use(swagger("My API", { bearerAuth: true }));

Router Options

Configure router-level defaults for OpenAPI documentation:

const UserRouter = TypedRouter(express.Router(), {
  tag: "Users", // Default tag for all routes in this router
  basePath: "/users", // Prefix for OpenAPI paths (for documentation only)
});

// All routes automatically tagged as "Users" in Swagger
UserRouter.get("/", { response: z.array(UserSchema) }, handler);
UserRouter.get("/:id", { response: UserSchema }, handler);

Schema Options

Add OpenAPI metadata directly in your route schema:

router.get(
  "/:id",
  {
    params: z.object({ id: z.string() }),
    response: UserSchema,
    tags: ["Users", "Public"], // Override router tag
    summary: "Get user by ID", // Endpoint summary
    description: "Returns a single user by their unique identifier",
  },
  handler,
);

Multiple Response Codes

Document different response schemas for different HTTP status codes:

router.get(
  "/:id",
  {
    params: z.object({ id: z.string() }),
    responses: {
      200: UserSchema,
      404: z.object({ error: z.string() }),
      500: z.object({ error: z.string(), requestId: z.string().optional() }),
    },
    summary: "Get user by ID",
  },
  handler,
);

You can also use both response and responses together:

{
  response: UserSchema,  // Shorthand for 200
  responses: {
    404: NotFoundSchema,
    500: ErrorSchema,
  },
}

Schema Metadata

Use .openapi() on any Zod schema to add descriptions, examples, and more (powered by zod-to-openapi):

const UserSchema = z
  .object({
    name: z.string().openapi({ example: "John" }),
    email: z.string().email().openapi({ example: "john@example.com" }),
  })
  .openapi({
    description: "A user object",
    example: { name: "John", email: "john@example.com" },
  });
Meebo.mp4

Configuration

Configure meebo globally to customize error responses and validation behavior:

import { configureMeebo } from "meebo";

configureMeebo({
  // Custom error response format
  formatError: (context) => ({
    success: false,
    error: `${context.type} validation failed`,
    details: context.zodError.issues.map((issue) => ({
      field: issue.path.join("."),
      message: issue.message,
    })),
  }),

  // Skip response validation for these status codes (default shown)
  skipResponseValidationForStatus: [400, 401, 403, 404, 409, 422, 500, 502, 503],

  // Disable response validation entirely (not recommended)
  validateResponses: true,
});

Error Context

The formatError function receives a context object:

interface MeeboErrorContext {
  type: "request" | "response" | "params" | "query" | "headers";
  method: string; // HTTP method (GET, POST, etc.)
  path: string; // Request path
  zodError: z.ZodError; // The Zod validation error
}

Default Error Format

Without configuration, errors are returned as:

{
  "error": "Request validation failed",
  "type": "request",
  "detail": [{ "path": ["email"], "message": "Invalid email" }]
}

License

ISC

About

The missing API validation library for Express

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors