{"_id":"@afarmer/frapi","_rev":"3-f40b6ca4c5842cd730dc598d52041a5c","name":"@afarmer/frapi","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@afarmer/frapi","version":"1.0.0","keywords":["express","zod","openapi","typescript","type-safe","api","routes","schema-validation"],"author":{"name":"afarmer"},"license":"MIT","_id":"@afarmer/frapi@1.0.0","maintainers":[{"name":"ahmi","email":"ahmet2@gmail.com"}],"homepage":"https://github.com/afarmerdev/frapi#readme","bugs":{"url":"https://github.com/afarmerdev/frapi/issues"},"dist":{"shasum":"ba26988347e7e33c79e1ed4ae9c14e6cab0c0d43","tarball":"https://registry.npmjs.org/@afarmer/frapi/-/frapi-1.0.0.tgz","fileCount":23,"integrity":"sha512-mgvv8mXXT+RH5ezV4aFD3w7Oyr7sjuqCjgwyJispgJuyPiBheNz5O36BQ5DU7LhtEpSBPtZdEdQYHbZhvncAaA==","signatures":[{"sig":"MEUCIQDQlXz/QgT+GnPCEkDeq9Y0KOGYvH9jceshb5xuyRqULQIgL0cJ0vx6kk+peCk3DDAH3fKMBGwxWceuMTx9SG0NJWc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":172873},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest --watch","type-check":"tsc --noEmit","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"ahmi","email":"ahmet2@gmail.com"},"repository":{"url":"git+https://github.com/afarmerdev/frapi.git","type":"git"},"_npmVersion":"10.9.2","description":"Type-safe Express route definitions with Zod validation and automatic OpenAPI generation","directories":{},"_nodeVersion":"23.11.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^3.25.0","vitest":"^3.2.0","express":"^4.21.2","supertest":"^7.1.4","typescript":"^5.9.0","@types/node":"^22.0.0","@types/express":"^4.17.21","@types/supertest":"^6.0.2","zod-to-json-schema":"^3.24.0"},"peerDependencies":{"zod":"^3.0.0","express":"^4.18.0 || ^5.0.0","zod-to-json-schema":"^3.20.0"},"_npmOperationalInternal":{"tmp":"tmp/frapi_1.0.0_1783377337859_0.6035027381733302","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@afarmer/frapi","version":"1.0.1","keywords":["express","zod","openapi","typescript","type-safe","api","routes","schema-validation"],"author":{"name":"afarmer"},"license":"MIT","_id":"@afarmer/frapi@1.0.1","maintainers":[{"name":"ahmi","email":"ahmet2@gmail.com"}],"homepage":"https://github.com/afarmerdev/frapi#readme","bugs":{"url":"https://github.com/afarmerdev/frapi/issues"},"dist":{"shasum":"bc7fdfea1a5c3216c1c7caab3be5b5cba634e3e2","tarball":"https://registry.npmjs.org/@afarmer/frapi/-/frapi-1.0.1.tgz","fileCount":23,"integrity":"sha512-v2w7ehJL3wmxsIgejTP/va9H8sUxsi3gztN+FHpiZstofiZCF3bu8+HF8iUuS9e8M0k3gNJUSE8x7OW/EU8ryQ==","signatures":[{"sig":"MEUCIQDq9G2VNBX47iJx9lnui37+mI7GjxoQWC7Wi4ij3XXvYgIgYlRWapVmQcUrjLTezfIiEBQ/7zxapirRi1TMic/tD9E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":172909},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"1c9f10f8fbede811221f549d7dc2cecbe2d1bc52","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest --watch","type-check":"tsc --noEmit","prepublishOnly":"npm test && npm run build"},"_npmUser":{"name":"ahmi","email":"ahmet2@gmail.com"},"repository":{"url":"git+https://github.com/afarmerdev/frapi.git","type":"git"},"_npmVersion":"10.9.2","description":"Type-safe Express route definitions with Zod validation and automatic OpenAPI generation","directories":{},"_nodeVersion":"23.11.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"^3.25.0","vitest":"^3.2.0","express":"^4.21.2","supertest":"^7.1.4","typescript":"^5.9.0","@types/node":"^22.0.0","@types/express":"^4.17.21","@types/supertest":"^6.0.2","zod-to-json-schema":"^3.24.0"},"peerDependencies":{"zod":"^3.0.0","express":"^4.18.0 || ^5.0.0","zod-to-json-schema":"^3.20.0"},"_npmOperationalInternal":{"tmp":"tmp/frapi_1.0.1_1783377876559_0.2844861584244236","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@afarmer/frapi","version":"1.0.2","description":"Type-safe Express route definitions with Zod validation and automatic OpenAPI generation","type":"module","main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"scripts":{"build":"tsc","type-check":"tsc --noEmit","test":"vitest run","test:watch":"vitest --watch","prepublishOnly":"npm test && npm run build"},"keywords":["express","zod","openapi","typescript","type-safe","api","routes","schema-validation"],"license":"MIT","author":{"name":"afarmer"},"repository":{"type":"git","url":"git+https://github.com/afarmerdev/frapi.git"},"bugs":{"url":"https://github.com/afarmerdev/frapi/issues"},"homepage":"https://github.com/afarmerdev/frapi#readme","engines":{"node":">=18"},"publishConfig":{"access":"public"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","zod":"^3.0.0","zod-to-json-schema":"^3.20.0"},"devDependencies":{"@types/express":"^4.17.21","@types/node":"^22.0.0","@types/supertest":"^6.0.2","express":"^4.21.2","supertest":"^7.1.4","typescript":"^5.9.0","vitest":"^3.2.0","zod":"^3.25.0","zod-to-json-schema":"^3.24.0"},"_id":"@afarmer/frapi@1.0.2","gitHead":"43afe99f686b76a340d1cf18adb40f047e29920e","_nodeVersion":"23.11.0","_npmVersion":"10.9.2","dist":{"integrity":"sha512-/S7pfpX8R/YFUsDq2k21abFQuuv7BjbkopK3TH7dfm7XnNWhA7JrtOocCZt9jsKZhOAS4LZ9jvwOE4i1ewuqPA==","shasum":"bcfdbb2c3923fc679af16849b8faf09ad3b8d372","tarball":"https://registry.npmjs.org/@afarmer/frapi/-/frapi-1.0.2.tgz","fileCount":23,"unpackedSize":172934,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDBmU/MBbj7vEEF04RLhHSnIouiziCEHRxElpXWvhRDpwIgKTt83ZksqkpToQkD+pw4/xry4uLi50SZNJEvqM6LBgk="}]},"_npmUser":{"name":"ahmi","email":"ahmet2@gmail.com"},"directories":{},"maintainers":[{"name":"ahmi","email":"ahmet2@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/frapi_1.0.2_1783378197780_0.03766167747640825"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-06T22:35:37.750Z","modified":"2026-07-06T22:49:58.064Z","1.0.0":"2026-07-06T22:35:37.991Z","1.0.1":"2026-07-06T22:44:36.699Z","1.0.2":"2026-07-06T22:49:57.941Z"},"bugs":{"url":"https://github.com/afarmerdev/frapi/issues"},"author":{"name":"afarmer"},"license":"MIT","homepage":"https://github.com/afarmerdev/frapi#readme","keywords":["express","zod","openapi","typescript","type-safe","api","routes","schema-validation"],"repository":{"type":"git","url":"git+https://github.com/afarmerdev/frapi.git"},"description":"Type-safe Express route definitions with Zod validation and automatic OpenAPI generation","maintainers":[{"name":"ahmi","email":"ahmet2@gmail.com"}],"readme":"# Frapi\n\n> Frapi (Freebnk API) was created at [Freebnk](https://freebnk.io) to incrementally refactor Freebnk's Express API endpoints while providing full type safety and automatic OpenAPI compatibility. It was designed to be simple and was built at a time when generative AI was not yet as capable as it is today, so most of the code was written by hand.\n>\n> Since then, newer frameworks have emerged. If you're looking for a more comprehensive solution that provides end-to-end type safety for both the backend and the client, I recommend ORPC: https://orpc.dev/.\n\nFrapi is a lightweight Zod based type-safety layer on top of express.\n\nIt automatically provides typing for handlers. Both input (route and query params, body, headers) and output (return body and status) types. In runtime it parses and validates according to respective Zod schemas.\n\nIt is possible to add express middlewares and enhance request context in a typesafe way.\n\nIt supports easy chaining of middlewares without losing typing.\n\nThe schemas drive both runtime validation and OpenAPI spec generation, so your types, validation, and docs always stay in sync.\n\n## Install\n\n```bash\nnpm install @afarmer/frapi zod zod-to-json-schema express\n```\n\n> `zod`, `zod-to-json-schema`, and `express` are peer dependencies.\n\n## Quick start\n\n```ts\nimport express from \"express\";\nimport { z } from \"zod\";\nimport { frapi, addRoutes, generateOpenapi } from \"@afarmer/frapi\";\n\n// 1. Define endpoints\nconst getUser = frapi.get(\"/users/:id\", {\n  schema: {\n    params: z.object({ id: z.string() }),\n    response: z.object({ id: z.string(), name: z.string() }),\n  },\n  handler: async ({\n    params, // <-- automatically typed {id:string}\n  }) => {\n    return { id: params.id, name: \"Alice\" }; // <-- return expects {id:string, name:string}\n  },\n});\n\nconst createUser = frapi.post(\"/users\", {\n  schema: {\n    body: z.object({ name: z.string().min(1), email: z.string().email() }),\n    response: z.object({ id: z.string() }),\n  },\n  handler: async ({\n    body, // <-- automatically typed {name:string, email:string}\n  }) => {\n    return { id: \"usr_123\" }; // <-- return expects {id:string}\n  },\n});\n\n// 2. Register them on an Express router\nconst app = express();\napp.use(express.json());\n\nconst router = express.Router();\naddRoutes(router, [getUser, createUser]);\napp.use(router);\n\n// 3. Serve OpenAPI JSON\napp.get(\"/open-api\", (_req, res) => {\n  res.json(\n    generateOpenapi({\n      info: \"My API\",\n      version: \"1.0.0\",\n      serverUrls: [\"http://localhost:3000\"],\n      endpoints: [getUser, createUser],\n    }),\n  );\n});\n\napp.listen(3000);\n```\n\n### Returning responses\n\nHandlers can return:\n\n- **A plain object** — automatically wrapped as `200 OK` with the object as JSON body.\n- **`null` or `undefined`** — sent as `204 No Content`.\n- **`FrapiResponse`** — for explicit status codes:\n\n```ts\nimport { FrapiResponse } from \"@afarmer/frapi\";\n\nFrapiResponse.ok(body); // 200\nFrapiResponse.created(body); // 201\nFrapiResponse.badRequest(body); // 400\nFrapiResponse.unauthorized(body); // 401\nFrapiResponse.forbidden(body); // 403\nFrapiResponse.notFound(body); // 404\nFrapiResponse.internalServerError(body); // 500\n```\n\n## Type safe middlewares & chaining\n\nUse `.with()` to attach Express middleware and enrich the request context with typed fields that your handlers and authorize hooks can access. This is how authentication, authorization, and other cross-cutting concerns are composed.\n\n```ts\nimport { frapi, type FrapiExpressMiddleware } from \"@afarmer/frapi\";\n\n// Define a middleware that adds `user` to the request context\nconst authMiddleware: FrapiExpressMiddleware<{ user: User }> = {\n  // define an express middleware\n  middleware: (endpoint) => {\n    return (req, res, next) => {\n      // your auth logic here\n      res.locals.user = { id: \"1\", name: \"Alice\", role: \"admin\" };\n      next();\n    };\n  },\n\n  // pick fields from express context that will be revealed in frapi context\n  enrichRequestContext: ({ res }) => ({\n    user: res.locals.user as User,\n  }),\n};\n\n// Chain middlewares to create a scoped API builder\nconst AuthApi = frapi.with(authMiddleware);\n\n// All endpoints created via AuthApi have `user` in their request context\nconst getProfile = AuthApi.get(\"/profile\", {\n  schema: {},\n  handler: async ({ user }) => {\n    // user is typed: { id: string, name: string, role: string }\n    return { id: user.id, name: user.name };\n  },\n});\n```\n\nYou can chain multiple middlewares:\n\n```ts\nconst SecureApi = frapi\n  .with(deviceInfoMiddleware)\n  .with(authMiddleware)\n  .with(roleMiddleware);\n\n// SecureApi.get(...), SecureApi.post(...), etc.\n```\n\n## Protecting endpoints with authorize hooks\n\nEvery endpoint accepts an optional `authorize` function that runs after request validation but before the handler. If it returns `false` (or resolves to `false`), the response is `403 Forbidden`.\n\n```ts\nconst transferFunds = frapi.post(\"/transfers\", {\n  authorize: async ({ body, user }) => {\n    const account = await db.getAccount(body.fromAccountId);\n    return account.ownerId === user?.id;\n  },\n  schema: {\n    body: z.object({\n      fromAccountId: z.string(),\n      toAccountId: z.string(),\n      amount: z.number().positive(),\n    }),\n  },\n  handler: async ({ body }) => {\n    return await processTransfer(body);\n  },\n});\n```\n\n### Reusable authorize functions\n\nYou can factory authorize hooks to share logic across endpoints:\n\n```ts\nfunction requireMinRole(minRole: \"admin\" | \"operator\" | \"viewer\") {\n  return ({ user }: { user: { id: string; role: string } }) => {\n    return user?.role === minRole;\n  };\n}\n\nconst getAccount = frapi.get(\"/accounts/:id\", {\n  authorize: requireMinRole(\"viewer\"),\n  schema: { params: z.object({ id: z.string() }) },\n  handler: async ({ params }) => ({ id: params.id }),\n});\n\nconst deleteAccount = frapi.delete(\"/accounts/:id\", {\n  authorize: requireMinRole(\"admin\"),\n  schema: { params: z.object({ id: z.string() }) },\n  handler: async ({ params }) => {\n    /* ... */\n  },\n});\n```\n\n## Generating OpenAPI JSON\n\nPass all your endpoints to `generateOpenapi` to produce an OpenAPI 3.1 spec.\n\n```ts\nimport { generateOpenapi } from \"@afarmer/frapi\";\n\nconst allEndpoints = [getUser, createUser, deleteUser, listUsers];\n\nconst openApiSpec = generateOpenapi({\n  info: \"My API\",\n  version: \"1.0.0\",\n  serverUrls: [\"http://localhost:3000\"],\n  endpoints: allEndpoints,\n});\n\n// Serve it\napp.get(\"/open-api\", (_req, res) => {\n  res.json(openApiSpec);\n});\n```\n\nEndpoints without `allowAnonymous: true` automatically get a `bearerAuth` security requirement. Path parameters, query parameters, request bodies, and response schemas are all derived from the Zod schemas.\n\n```ts\n// Public endpoint — no security requirement in OpenAPI\nconst healthCheck = frapi.get(\"/health\", {\n  allowAnonymous: true,\n  schema: {\n    response: z.object({ status: z.string() }),\n  },\n  handler: () => ({ status: \"ok\" }),\n});\n\n// Protected endpoint — bearerAuth security in OpenAPI\nconst getSecret = frapi.get(\"/secret\", {\n  schema: {\n    response: z.object({ secret: z.string() }),\n  },\n  handler: () => ({ secret: \"shh\" }),\n});\n```\n\n## Error handling\n\nfrapi includes `AppError` and `ForbiddenError` classes that are handled automatically in route handlers:\n\n```ts\nimport { AppError, ForbiddenError } from \"@afarmer/frapi\";\n\nconst getUser = frapi.get(\"/users/:id\", {\n  schema: { params: z.object({ id: z.string() }) },\n  handler: async ({ params }) => {\n    const user = await db.findUser(params.id);\n    AppError.assert(user, \"User not found\", \"user_not_found\");\n    ForbiddenError.assert(user.active, \"User is not active\");\n    return user;\n  },\n});\n```\n\n- `AppError` produces a `400` response: `{ appError: true, message, messageCode }`\n- `ForbiddenError` produces a `403` response: `{ error, messageCode }`\n\n### `addRoute` options\n\n```ts\nimport { addRoute } from \"@afarmer/frapi\";\n\naddRoute(router, endpoint, {\n  responseValidationMode: \"validate\", // \"validate\" (default) | \"observe\" | \"ignore\"\n  handleErrors: true, // catch unhandled errors and return 500\n  onError: (error, endpoint) => {\n    logger.error(`Error in ${endpoint.method} ${endpoint.path}:`, error);\n  },\n});\n```\n\n## Response validation modes\n\nfrapi validates response bodies against the Zod schemas defined in your endpoint. The behavior when a response doesn't match its schema is configurable via `responseValidationMode` in `addRoute` / `addRoutes`:\n\n| Mode       | Behavior                                                                                                                                 | Best for                                  |\n| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |\n| `validate` | Returns `500` on schema mismatch — the mismatched response is not sent to the client.                                                    | Greenfield projects                       |\n| `observe`  | Sends the response as-is, sets the `x-schema-compliance` header, and calls `onError` if provided. Mismatches should be logged and fixed. | Brownfield projects, incremental adoption |\n| `ignore`   | Skips response validation entirely. No headers, no checks.                                                                               | Full YOLO approach                        |\n\n**Default is `validate`.**\n\n### `validate` — strict mode (greenfield)\n\nFor new projects where response schemas are carefully defined, `validate` ensures the client never receives a response that doesn't match the documented schema. If the handler returns something that fails Zod validation, the request fails with `500`:\n\n```ts\naddRoutes(router, endpoints, {\n  responseValidationMode: \"validate\",\n  onError: (error, endpoint) => {\n    // called on schema mismatch before the 500 is sent\n    logger.error(\n      `Response validation failed for ${endpoint.method} ${endpoint.path}`,\n      error,\n    );\n  },\n});\n```\n\n### `observe` — report mode (brownfield / incremental adoption)\n\nFor existing projects where response typing wasn't carefully implemented, `observe` lets you add schemas without breaking running endpoints. Mismatches are surfaced via the `x-schema-compliance` response header (`success`, `failed`, or `lack-of-schema`) and reported through `onError`, but the response is still sent:\n\n```ts\naddRoutes(router, endpoints, {\n  responseValidationMode: \"observe\",\n  onError: (error, endpoint) => {\n    // log mismatches so they can be fixed incrementally\n    logger.warn(\n      `Schema mismatch in ${endpoint.method} ${endpoint.path}`,\n      error,\n    );\n  },\n});\n```\n\n### `ignore` — skip validation\n\nNo response validation runs. No `x-schema-compliance` header is set. Use this if you want request validation and OpenAPI generation but don't want response checking:\n\n```ts\naddRoutes(router, endpoints, {\n  responseValidationMode: \"ignore\",\n});\n```\n\n## API reference\n\n### Schema fields\n\n| Field          | Description                                    |\n| -------------- | ---------------------------------------------- |\n| `params`       | Path parameters (`:id` in the route path)      |\n| `query`        | Query string parameters                        |\n| `body`         | Request body (only allowed for POST/PUT/PATCH) |\n| `headers`      | Request headers                                |\n| `response`     | 200 response body schema                       |\n| `response_400` | 400 Bad Request response body schema           |\n| `response_401` | 401 Unauthorized response body schema          |\n| `response_402` | 402 Validation error response body schema      |\n| `response_403` | 403 Forbidden response body schema             |\n| `response_404` | 404 Not Found response body schema             |\n\nEach field accepts either a `z.object({...})` or a plain shape object `{ key: z.string() }` (wrapped automatically).\n\n### Validation behavior\n\n- Request params/query/body/headers are validated with Zod **before** the handler runs. Invalid requests get `402` with `{ errors: [...] }`.\n- Response bodies are validated against the `response` schema after the handler runs.\n\n### `frapi`\n\nDefault export. A function with `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, and `.with()` methods for defining endpoints.\n\n### `addRoute(router, endpoint, options?)`\n\nRegisters a single frapi endpoint on an Express router. Options: `responseValidationMode` (see [Incremental adoption](#incremental-adoption)), `handleErrors`, `onError`.\n\n### `addRoutes(router, endpoints[], options?)`\n\nRegisters multiple frapi endpoints on an Express router. Same options as `addRoute`.\n\n### `generateOpenapi({ info, version, serverUrls, endpoints })`\n\nReturns an OpenAPI 3.1 spec object from the given endpoints.\n\n### `FrapiResponse`\n\nClass with static factory methods: `.ok()`, `.created()`, `.badRequest()`, `.unauthorized()`, `.forbidden()`, `.notFound()`, `.internalServerError()`.\n\n### `AppError`, `ForbiddenError`\n\nError classes with `.assert()` and `.on()` static methods, handled automatically by the Express integration.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}