Express.js workflow with automatic type safety, request validation, and OpenAPI documentation from a single source of truth

Express.js remains the backbone of Node.js API development in 2025 for good reason: it's stable, widely adopted, and battle-tested across millions of production applications. But if you're building modern APIs with Express, you've likely encountered the same frustrating workflow:

  1. Define TypeScript interfaces for your request/response data
  2. Implement separate validation logic with libraries like express-validator
  3. Write and maintain OpenAPI documentation that duplicates both of the above
  4. Constantly synchronize changes across all three systems

This approach leads to a duplication problem:

// Type definition
interface UserParams {
  id: number;
}

// Documentation
/**
 * @swagger
 * /users/{id}:
 *   get:
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: User details
 */
app.get(
  "/users/:id",
  // Validation (separate from types)
  [param("id").isInt().toInt(), validationMiddleware],
  (req, res) => {
    const { id } = req.params as UserParams; // Type assertion
    // ...
  }
);

Many teams consider abandoning Express entirely for newer frameworks like NestJS, Fastify, or TSOA, but this comes with significant drawbacks:

  • Complete codebase rewrites
  • Team retraining costs
  • Risk of framework abandonment
  • Smaller community support
  • Less mature ecosystems

Introducing tyex

What if you could keep your Express expertise while gaining all the benefits of modern frameworks?

tyex is a lightweight library that adds TypeScript type safety, automatic request validation, and OpenAPI documentation generation to your existing Express applications. All without requiring a rewrite.

Instead of maintaining three separate systems, tyex allows you to define everything in one place.

Real-World Example: Building a Modern Bookshelf API

Let's build a complete API to demonstrate tyex in action:

1. Project Setup

mkdir bookshelf-api
cd bookshelf-api
npm init -y
npm install express tyex @sinclair/typebox swagger-ui-express
npm install --save-dev typescript @types/express@4 @types/node@22 @types/swagger-ui-express tsx
npx tsc --init

2. Define Your Data Models With TypeBox

TypeBox creates JSON Schema objects that simultaneously infer as TypeScript types:

// src/schemas/book.schema.ts
import { Type, type Static } from "@sinclair/typebox";
import { TypeOpenAPI } from "tyex";

export const BookSchema = Type.Object({
  id: Type.String({ format: "uuid" }),
  title: Type.String(),
  author: Type.String(),
  year: Type.Integer({ minimum: 0 }),
  genre: TypeOpenAPI.StringEnum([
    "fiction",
    "non-fiction",
    "science",
    "technology",
    "history",
    "biography",
  ]),
  available: Type.Boolean(),
  createdAt: Type.String({ format: "date-time" }),
  updatedAt: TypeOpenAPI.Nullable(Type.String({ format: "date-time" })),
});

export type Book = Static<typeof BookSchema>;

export const CreateBookSchema = Type.Omit(BookSchema, [
  "id",
  "createdAt",
  "updatedAt",
]);

export type CreateBook = Static<typeof CreateBookSchema>;

export const UpdateBookSchema = Type.Partial(CreateBookSchema);

export type UpdateBook = Static<typeof UpdateBookSchema>;

3. Create Type-Safe Controllers With Automatic Validation

The tyex.handler function wraps your Express route handlers, providing runtime validation, type safety, and OpenAPI documentation generation:

// src/controllers/book.controller.ts
import { Type } from "@sinclair/typebox";
import tyex from "tyex";
import {
  BookSchema,
  CreateBookSchema,
  UpdateBookSchema,
} from "../schemas/book.schema";
import { BookService } from "../services/book.service";

export const BookController = {
  getAll: tyex.handler(
    {
      tags: ["books"],
      summary: "Get all books",
      parameters: [
        {
          in: "query",
          name: "genre",
          required: false,
          schema: Type.String(),
        },
      ],
      responses: {
        "200": {
          description: "List of books",
          content: {
            "application/json": {
              schema: Type.Array(BookSchema),
            },
          },
        },
      },
    },
    async (req, res) => {
      let books = await BookService.getAll();

      // Apply genre filter if provided
      if (req.query.genre) {
        books = books.filter((book) => book.genre === req.query.genre);
      }

      res.json(books);
    }
  ),

  // Additional handlers...
  getById: tyex
    .handler
    /* ... */
    (),

  create: tyex
    .handler
    /* ... */
    (),

  update: tyex
    .handler
    /* ... */
    (),

  delete: tyex
    .handler
    /* ... */
    (),
};

4. Set Up Your Routes

Your routes remain clean and familiar, using the Express syntax you already know:

// src/routes/book.routes.ts
import express from "express";
import { BookController } from "../controllers/book.controller";

const router = express.Router();

router.get("/", BookController.getAll);
router.get("/:id", BookController.getById);
router.post("/", BookController.create);
router.put("/:id", BookController.update);
router.delete("/:id", BookController.delete);

export default router;

5. Automatic OpenAPI Documentation

The tyex.openapi middleware automatically generates an OpenAPI document from all your handlers:

// src/config/openapi.ts
import tyex from "tyex";

export const openapiConfig = tyex.openapi({
  document: {
    openapi: "3.0.3",
    info: {
      title: "Bookshelf API",
      description: "A simple API for managing books",
      version: "1.0.0",
      contact: {
        name: "API Support",
        email: "[email protected]",
      },
      license: {
        name: "MIT",
        url: "https://opensource.org/licenses/MIT",
      },
    },
    tags: [
      {
        name: "books",
        description: "Operations for managing books",
      },
    ],
  },
});

6. Complete Application Setup

// src/index.ts
import express from "express";
import swaggerUi from "swagger-ui-express";
import { ValidationError } from "tyex";
import { openapiConfig } from "./config/openapi";
import bookRoutes from "./routes/book.routes";

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(express.json());

// Routes
app.use("/api/books", bookRoutes);

// OpenAPI documentation
app.get("/api-docs/spec", openapiConfig);
app.use(
  "/api-docs",
  swaggerUi.serve,
  swaggerUi.setup(null, {
    swaggerOptions: { url: "/api-docs/spec" },
  })
);

// Error handler for validation errors
app.use((err, req, res, next) => {
  if (err instanceof ValidationError) {
    return res.status(400).json({
      error: "Validation Error",
      details: err.errors,
    });
  }

  console.error(err);
  res.status(500).json({ error: "Internal Server Error" });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
  console.log(
    `API documentation available at http://localhost:${PORT}/api-docs`
  );
});

Once your application is running, visit http://localhost:300/api-docs to see your automatically generated interactive API documentation:

Preview of Generated OpenAPI Documentation

Unlock the OpenAPI Ecosystem

With tyex's automatic OpenAPI generation, you instantly get access to a rich ecosystem of tools:

  • Interactive API Documentation with Swagger UI
  • Automatic Client Generation in TypeScript, Python, Java, and more
  • Seamless Testing Integration with Postman and other API tools
  • Contract Testing to ensure API compliance

Why Choose tyex Over Framework Rewrites

1. Gradual Adoption

Migrate endpoints one at a time - no need for a complete rewrite.

2. Leverages Existing Knowledge

Your team already knows Express - tyex builds on that knowledge rather than replacing it.

3. Maximum Type Safety

Full TypeScript inference for all request and response objects.

4. Single Source of Truth

Define your API contract once, eliminating consistency issues between types, validation, and documentation.

5. Framework Independence

Not locked into a specific framework's opinions or lifecycle.

Perfect For

  • Existing Express Applications seeking modernization without rewriting
  • Teams With Express Expertise who want to leverage their existing knowledge
  • Microservice Architectures needing consistent API documentation across services
  • Projects With Tight Timelines that can't afford a framework migration

Conclusion

In the fast-paced world of JavaScript frameworks, tyex offers a refreshing alternative to complete rewrites. By enhancing Express with the features developers need most - type safety, validation, and documentation - tyex delivers the best of both worlds.

Give tyex a try for your next Express API project.


The complete source code for this example is available on GitHub.