{"id":21347305,"url":"https://github.com/ziolko/frapi","last_synced_at":"2025-07-12T17:31:55.644Z","repository":{"id":39405069,"uuid":"493795673","full_name":"ziolko/frapi","owner":"ziolko","description":"Automatically create client-side functions to consume your API","archived":false,"fork":false,"pushed_at":"2022-07-02T18:46:28.000Z","size":1561,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-07-10T15:56:10.896Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ziolko.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-18T19:15:20.000Z","updated_at":"2022-06-24T08:59:11.000Z","dependencies_parsed_at":"2022-09-08T13:22:54.321Z","dependency_job_id":null,"html_url":"https://github.com/ziolko/frapi","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ziolko/frapi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Ffrapi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Ffrapi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Ffrapi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Ffrapi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ziolko","download_url":"https://codeload.github.com/ziolko/frapi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ziolko%2Ffrapi/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264979513,"owners_count":23692481,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-22T02:13:38.423Z","updated_at":"2025-07-12T17:31:55.364Z","avatar_url":"https://github.com/ziolko.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n# Introduction\n\nFrapi is middleware and router for Express providing the following features:\n\n- validation of request payload and query parameters,\n- validation of response payload,\n- automatic generation of an API client library,\n- fully blown Typescript support (both on backend and in the generated client library),\n- catching errors in asynchronous request handlers.\n\nYou can find a working example presenting all of the above in\n[this CodeSandbox](https://codesandbox.io/s/ancient-morning-cd4vbo?file=/index.ts).\n\n## Installation\n\nInstall the package as a dependency with `npm` or `yarn`:\n\n- `npm install frapi`\n- `yarn add frapi`\n\n## Basic usage\n\n```typescript\nimport express from \"express\";\nimport bodyParser from \"body-parser\";\nimport { Router } from \"frapi\";\n\nconst app = express().use(bodyParser.json());\n\n// Create frapi router and attach it to the express app\nconst routes = new Router();\napp.use(routes);\n\nroutes.get(\n    {\n        path: \"/user/:id\",\n        // Define expected result shape. \n        // You can also define shapes of request payload and query parameters.\n        response: { fullName: String, age: Number } \n    },\n    (req, res) =\u003e {\n        res.sendResponse({ fullName: \"John Smith\", age: 12 });\n    }\n);\n\napp.listen(3000);\n```\n\n## Generating client library\n\nYou can generate a strongly typed client library based on the endpoints definition. Let's take the following example:\n\n```typescript\nimport express from \"express\";\nimport bodyParser from \"body-parser\";\nimport { Router, saveEndpointsToFile } from \"frapi\";\n\nconst app = express().use(bodyParser.json());\n\n// Create frapi router and attach it to the express app\nconst routes = new Router();\napp.use(routes);\n\nroutes.post(\n    {\n        path: \"/user/:id\",\n        name: 'createUser',\n        body: { fullName: String, age: Number },\n        response: { id: String, fullName: String, age: Number } \n    },\n    (req, res) =\u003e {\n        res.sendResponse({ ...req.body, id: req.params.id });\n    }\n);\n\n// This traverses all the registered endpoints in the app and \n// generates a strongly typed client library\nsaveEndpointsToFile(app, \"./api.ts\", \"ts\")\n\napp.listen(3000);\n```\n\nThe code above will generate the following client library (`api.ts`):\n\n```typescript\nexport async function createUser(id: string, body: { fullName: string; age: number }) {\n    const response = await fetch(`/user/${id}`, { method: 'post',  headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), });\n    const responseBody = (await response.json()) as { id: string; fullName: string; age: number };\n    return { ok: response.ok, status: response.status, body: responseBody, headers: response.headers, response };\n}\n```\n\nBoth, request payload and response body are strongly typed. This establishes the contract between\nyour backend and frontend.\n\n\u003e :warning: The `saveEndpointsToFile` function implementation is currently in the proof-of-concept state.\n\u003e All contributions are highly appreciated!\n\n## Validation syntax\n\nThe validation API uses `String`, `Boolean` and `Number`  constructors to define primitive types.\nNested structures are described as nested objects. For example:\n\n```typescript\nconst User = {\n    name: String,\n    age: Number,\n    isAdult: Boolean,\n    address: {\n        firstLine: String,\n        secondLine: String,\n    }\n}\n```\n\nOptional fields are defined by adding `?` to their name. In the example below\nboth `name` and `surname` are optional (can be `undefined`):\n\n```typescript\nconst User = {\n    \"name?\": String,\n    \"surname?\": String,\n}\n```\n\nThere's a number of helpers to define complex types:\n\n- `ArrayOf(Type)` defines an array of objects of given types (`Type[]` in TypeScript).\n- `MapOf(Type)` defines an map with values of provided type (`Record\u003cString, Type\u003e` in TypeScript).\n- `AnyOf(A, B, C)` defines a union of types (`A | B | C` in TypeScript).\n- `AllOf(A, B, C)` defines an intersection of types (`A \u0026 B \u0026 C` in TypeScript).\n\nExample:\n\n```typescript\nimport { ArrayOf, AnyOf } from 'frapi';\n\nconst Book = {\n    title: String,\n    author: String,\n};\n\nconst User = {\n    name: String,\n    books: ArrayOf(Book),                       \n    country: AnyOf('US' as const, 'UK' as const) \n}\n```\n\n### Custom validators\n\nYou can define custom validation logic with the following syntax:\n\n```typescript\nconst NonEmptyString = {\n  // The underlying type for TypeScript\n  $type: String,\n  // Validation method. Returns true, if the object is valid and false otherwise.\n  // Can also throw an exception with validation error deatils \n  $validate: (text: string) =\u003e text.trim().length \u003e 0\n};\n```\n\n### Remarks\n\nFields starting with `$` are ignored during validation - you can't expect e.g. to successfully validate\nan payload with a field `$name`.\n\nFrapi rejects objects with fields that are not defined in the validation type. E.g. the following code will fail:\n\n```typescript\nimport { validate } from 'frapi';\n    \nconst User = { name: String, age: Number };\n\n// Error - the field `city` is not in the type definition. \nvalidate(User, { name: \"John\", age: 25, city: 'London '})\n```\n\n\n### Acknowledgements\n\nThis project is built based on `express-list-endpoints` and `express-async-router`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fziolko%2Ffrapi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fziolko%2Ffrapi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fziolko%2Ffrapi/lists"}