A declarative TypeScript framework for building type-safe Express.js APIs with automatic OpenAPI generation
npm install transit-kit





A declarative TypeScript framework for building end-to-end type-safe REST APIs with Express.js and automatic OpenAPI documentation generation.
Transit-Kit brings modern type-safety and developer experience to Express.js without abandoning the familiar, battle-tested foundation you already know. It's built on minimalism:
- ðŊ Express at its core â Use the familiar Express API you already know
- ð End-to-end type safety â Full TypeScript inference from request to response
- ð Auto-generated OpenAPI â Documentation that stays in sync with your code
- â
Zod validation â Runtime type checking with zero boilerplate
- ðŠķ Minimal overhead â Thin layer over Express, not a complete rewrite
- Declarative definitions â Define endpoints declaratively with full type info
- Node.js 22+
- TypeScript 5.9+
``bash`
npm install transit-kit
`bash`
yarn add transit-kit
`bash`
pnpm add transit-kit
`typescript
import { createServer } from "transit-kit/server";
import { createApiEndpointHandler } from "transit-kit/server";
import { z } from "zod";
// Create a server
const server = createServer({
port: 3000,
});
// Define a simple endpoint
const helloEndpoint = createApiEndpointHandler(
{
meta: {
name: "sayHello",
group: "Greetings",
description: "Returns a greeting message",
},
path: "/hello/:name",
method: "get",
responseSchemas: {
200: {
type: "json",
schema: z.object({
message: z.string(),
}),
},
},
securitySchemes: [],
},
async ({ parameters }) => {
return {
code: 200,
data: {
message: Hello, ${parameters.name}!,
},
};
}
);
// Register and start
server.registerApiEndpoint(helloEndpoint);
server.start();
`
Every API endpoint in Transit-Kit is defined declaratively with full type information:
`typescript`
const definition = {
meta: {
name: "createUser", // Unique operation ID
group: "Users", // OpenAPI tag
description: "Create a user" // Endpoint description
},
path: "/users", // Express-style path
method: "post", // HTTP method
requestBodySchema: z.object({ // Request validation (optional)
name: z.string(),
email: z.string().email(),
}),
querySchema: z.object({ // Query params validation (optional)
sendEmail: z.boolean().optional(),
}),
responseSchemas: { // All possible responses
201: {
type: "json",
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
},
400: {
type: "json",
schema: z.object({
error: z.string(),
}),
},
},
securitySchemes: [], // Auth schemes (if any)
};
Handlers automatically infer types from your endpoint definition:
`typescript`
const handler = async ({ body, query, parameters }) => {
// body is typed as { name: string, email: string }
// query is typed as { sendEmail?: boolean }
// parameters are inferred from the path pattern
const user = await createUserInDatabase(body);
return {
code: 201,
data: user, // Must match the response schema for code 201
};
};
Combine definitions and handlers, then register with your server:
`typescript`
const endpoint = createApiEndpointHandler(definition, handler);
server.registerApiEndpoint(endpoint);
Here's a complete example showing a REST API for managing todos:
`typescript
import { createServer, createApiEndpointHandler } from "transit-kit/server";
import { z } from "zod";
const server = createServer({
port: 3000,
});
// In-memory storage (use a real database in production)
const todos: Map
// Schemas
const TodoSchema = z.object({
id: z.string(),
title: z.string(),
completed: z.boolean(),
});
const CreateTodoSchema = z.object({
title: z.string().min(1),
});
// CREATE
const createTodo = createApiEndpointHandler(
{
meta: {
name: "createTodo",
group: "Todos",
description: "Create a new todo item",
},
path: "/todos",
method: "post",
requestBodySchema: CreateTodoSchema,
responseSchemas: {
201: {
type: "json",
schema: TodoSchema,
},
},
securitySchemes: [],
},
async ({ body }) => {
const id = crypto.randomUUID();
const todo = {
id,
title: body.title,
completed: false,
};
todos.set(id, todo);
return {
code: 201,
data: todo,
};
}
);
// READ (List)
const listTodos = createApiEndpointHandler(
{
meta: {
name: "listTodos",
group: "Todos",
description: "Get all todo items",
},
path: "/todos",
method: "get",
responseSchemas: {
200: {
type: "json",
schema: z.object({
todos: z.array(TodoSchema),
}),
},
},
securitySchemes: [],
},
async () => {
return {
code: 200,
data: {
todos: Array.from(todos.values()),
},
};
}
);
// READ (Single)
const getTodo = createApiEndpointHandler(
{
meta: {
name: "getTodo",
group: "Todos",
description: "Get a specific todo item",
},
path: "/todos/:id",
method: "get",
responseSchemas: {
200: {
type: "json",
schema: TodoSchema,
},
404: {
type: "json",
schema: z.object({ error: z.string() }),
},
},
securitySchemes: [],
},
async ({ parameters }) => {
const todo = todos.get(parameters.id);
if (!todo) {
return {
code: 404,
data: { error: "Todo not found" },
};
}
return {
code: 200,
data: todo,
};
}
);
// UPDATE
const updateTodo = createApiEndpointHandler(
{
meta: {
name: "updateTodo",
group: "Todos",
description: "Update a todo item",
},
path: "/todos/:id",
method: "put",
requestBodySchema: z.object({
title: z.string().optional(),
completed: z.boolean().optional(),
}),
responseSchemas: {
200: {
type: "json",
schema: TodoSchema,
},
404: {
type: "json",
schema: z.object({ error: z.string() }),
},
},
securitySchemes: [],
},
async ({ parameters, body }) => {
const todo = todos.get(parameters.id);
if (!todo) {
return {
code: 404,
data: { error: "Todo not found" },
};
}
const updated = {
...todo,
...body,
};
todos.set(parameters.id, updated);
return {
code: 200,
data: updated,
};
}
);
// DELETE
const deleteTodo = createApiEndpointHandler(
{
meta: {
name: "deleteTodo",
group: "Todos",
description: "Delete a todo item",
},
path: "/todos/:id",
method: "delete",
responseSchemas: {
204: {
type: "json",
schema: z.object({}),
},
404: {
type: "json",
schema: z.object({ error: z.string() }),
},
},
securitySchemes: [],
},
async ({ parameters }) => {
const exists = todos.has(parameters.id);
if (!exists) {
return {
code: 404,
data: { error: "Todo not found" },
};
}
todos.delete(parameters.id);
return {
code: 204,
data: {},
};
}
);
// Register all endpoints
server.registerApiEndpoint(createTodo);
server.registerApiEndpoint(listTodos);
server.registerApiEndpoint(getTodo);
server.registerApiEndpoint(updateTodo);
server.registerApiEndpoint(deleteTodo);
server.start();
console.log("Server running on http://localhost:3000");
`
Transit-Kit supports Basic and Bearer authentication schemes out of the box.
`typescript
import { createBasicAuthSchema } from "transit-kit/server";
// Define your user type
interface User {
id: string;
username: string;
role: string;
}
// Create an auth scheme
const basicAuth = createBasicAuthSchema
"basicAuth",
async (username, password) => {
// Validate credentials (use your database)
if (username === "admin" && password === "secret") {
return {
id: "1",
username: "admin",
role: "admin",
};
}
return null; // Invalid credentials
}
);
`
`typescript
import { createBearerAuthSchema } from "transit-kit/server";
const bearerAuth = createBearerAuthSchema
"bearerAuth",
async (token) => {
// Validate token (e.g., JWT verification)
const user = await verifyJWT(token);
return user; // or null if invalid
}
);
`
`typescript
const protectedEndpoint = createApiEndpointHandler(
{
meta: {
name: "getProfile",
group: "Users",
description: "Get current user profile",
},
path: "/profile",
method: "get",
responseSchemas: {
200: {
type: "json",
schema: z.object({
id: z.string(),
username: z.string(),
role: z.string(),
}),
},
401: {
type: "json",
schema: z.object({ error: z.string() }),
},
},
securitySchemes: [basicAuth], // Require authentication
},
async ({ caller }) => {
// caller is typed as User | null
if (!caller) {
return {
code: 401,
data: { error: "Unauthorized" },
};
}
return {
code: 200,
data: caller, // Fully typed user object
};
}
);
server.registerApiEndpoint(protectedEndpoint);
`
You can support multiple authentication methods:
`typescript`
const endpoint = createApiEndpointHandler(
{
// ... other config
securitySchemes: [basicAuth, bearerAuth], // Accept either
},
async ({ caller }) => {
// caller will be set if any scheme succeeds
// ...
}
);
Transit-Kit automatically generates OpenAPI 3.0 documentation from your endpoint definitions.
`typescript
import { generateOpenApiDoc } from "transit-kit/generator";
const openApiDoc = await generateOpenApiDoc(server, {
title: "My API",
version: "1.0.0",
description: "A type-safe REST API built with Transit-Kit",
servers: [
{
url: "http://localhost:3000",
description: "Development server",
},
{
url: "https://api.example.com",
description: "Production server",
},
],
contact: {
name: "API Support",
email: "support@example.com",
},
license: {
name: "MIT",
url: "https://opensource.org/licenses/MIT",
},
});
// Serve the OpenAPI spec
server.expressApp.get("/openapi.json", (req, res) => {
res.json(openApiDoc);
});
// Or write to file
import { writeFileSync } from "fs";
writeFileSync("./openapi.json", JSON.stringify(openApiDoc, null, 2));
`
The generated OpenAPI document includes:
- All endpoints with request/response schemas
- Path and query parameters
- Request body validation schemas
- Security requirements
- Response schemas for all status codes
You can use this with tools like:
- Swagger UI for interactive documentation
- Postman for API testing
- OpenAPI Generator for client SDK generation
#### createServer(config: ServerConfig): Server
Creates a new Transit-Kit server instance.
Config Options:
- port: number â Port to listen on
Returns: Server instance with:
- expressApp: Application â Underlying Express appregisterApiEndpoint(endpoint)
- â Register an API endpointstart()
- â Start the serverendpointDefinitions
- â Array of registered endpoint definitions
#### createApiEndpointHandler(definition, handler)
Creates a type-safe API endpoint.
Parameters:
- definition â Endpoint definition objecthandler
- â Async function handling the request
Handler receives:
- request â Express Request objectresponse
- â Express Response objectparameters
- â Type-safe path parametersquery
- â Type-safe query parametersbody
- â Type-safe request bodycaller
- â Authenticated user (if auth is enabled)
#### createBasicAuthSchema
Creates a Basic authentication scheme.
Parameters:
- name: string â Unique name for the schemevalidateCaller: (username, password) => Promise
- â Validation function
#### createBearerAuthSchema
Creates a Bearer token authentication scheme.
Parameters:
- name: string â Unique name for the schemevalidateCaller: (token) => Promise
- â Validation function
#### generateOpenApiDoc(server, options): Promise
Generates OpenAPI 3.0 documentation.
Parameters:
- server: Server â Your Transit-Kit server instanceoptions: GeneratorOptions
- â OpenAPI metadata
Options:
- title: string â API titleversion: string
- â API versiondescription?: string
- â API descriptionservers?: ServerObject[]
- â Server URLscontact?: ContactObject
- â Contact informationlicense?: LicenseObject
- â License information
Transit-Kit currently supports JSON responses:
`typescript`
{
type: "json",
schema: z.object({ / your schema / }),
headers?: ["X-Custom-Header"], // Optional custom headers
}
When using custom headers in your response schema, you must include them in the handler response:
`typescript`
return {
code: 200,
data: { / ... / },
headers: {
"X-Custom-Header": "value",
},
};
Since Transit-Kit is built on Express, you can access the underlying Express app:
`typescript
const server = createServer({ / ... / });
// Add custom middleware
server.expressApp.use(cors());
server.expressApp.use(helmet());
// Static files
server.expressApp.use(express.static("public"));
// Custom routes
server.expressApp.get("/health", (req, res) => {
res.json({ status: "ok" });
});
`
Transit-Kit exports two main modules:
- transit-kit/server â Server creation, endpoint handlers, and authentication
- transit-kit/generator â OpenAPI documentation generation
For the best experience, ensure your tsconfig.json includes:
`json``
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"target": "ES2022",
"module": "ES2022"
}
}
MIT ÂĐ D4rkr34lm
Contributions are welcome! Please feel free to submit a Pull Request.