{"id":15118090,"url":"https://github.com/Bekacru/better-call","last_synced_at":"2025-09-27T23:32:05.706Z","repository":{"id":250382520,"uuid":"834313691","full_name":"Bekacru/better-call","owner":"Bekacru","description":"a tiny web framework for typescript","archived":false,"fork":false,"pushed_at":"2025-09-14T08:11:21.000Z","size":717,"stargazers_count":490,"open_issues_count":19,"forks_count":26,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-09-24T22:51:36.621Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Bekacru.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-07-26T23:16:39.000Z","updated_at":"2025-09-24T20:36:28.000Z","dependencies_parsed_at":"2024-07-27T00:28:23.687Z","dependency_job_id":"03c664fd-1dc0-4d86-ad1f-3083659a8f15","html_url":"https://github.com/Bekacru/better-call","commit_stats":null,"previous_names":["bekacru/better-call"],"tags_count":164,"template":false,"template_full_name":null,"purl":"pkg:github/Bekacru/better-call","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bekacru%2Fbetter-call","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bekacru%2Fbetter-call/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bekacru%2Fbetter-call/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bekacru%2Fbetter-call/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Bekacru","download_url":"https://codeload.github.com/Bekacru/better-call/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Bekacru%2Fbetter-call/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277303362,"owners_count":25795626,"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","status":"online","status_checked_at":"2025-09-27T02:00:08.978Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-09-26T01:46:07.902Z","updated_at":"2025-09-27T23:32:05.700Z","avatar_url":"https://github.com/Bekacru.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","Uncategorized"],"sub_categories":["Uncategorized"],"readme":"# Better Call\n\nBetter call is a tiny web framework for creating endpoints that can be invoked as a normal function or mounted to a router to be served by any web standard compatible server (like Bun, node, nextjs, sveltekit...) and also includes a typed RPC client for typesafe client-side invocation of these endpoints.\n\nBuilt for typescript and it comes with a very high performance router based on [rou3](https://github.com/unjs/rou3).\n\n## Install\n\n```bash\npnpm i better-call\n```\n\nMake sure to install [standard schema](https://github.com/standard-schema/standard-schema) compatible validation library like zod.\n\n```bash\npnpm i zod\n```\n\n## Usage\n\nThe building blocks for better-call are endpoints. You can create an endpoint by calling `createEndpoint` and passing it a path, [options](#endpointoptions) and a handler that will be invoked when the endpoint is called.\n\n```ts\nimport { createEndpoint, createRouter } from \"better-call\"\nimport { z } from \"zod\"\n\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n\n// Now you can call the endpoint just as a normal function.\nconst item = await createItem({\n    body: {\n        id: \"123\"\n    }\n})\n```\n\nOR you can mount the endpoint to a router and serve it with any web standard compatible server. \n\n\u003e The example below uses [Bun](https://bun.sh/)\n\n```ts\nconst router = createRouter({\n    createItem\n})\n\nBun.serve({\n    fetch: router.handler\n})\n```\n\nThen you can use the rpc client to call the endpoints on client.\n\n```ts\n//client.ts\nimport type { router } from \"./router\" // import router type\nimport { createClient } from \"better-call/client\";\n\nconst client = createClient\u003ctypeof router\u003e({\n    baseURL: \"http://localhost:3000\"\n});\nconst items = await client(\"/item\", {\n    body: {\n        id: \"123\"\n    }\n});\n```\n\n### Returning non 200 responses\n\nTo return a non 200 response, you will need to throw Better Call's `APIError` error. If the endpoint is called as a function, the error will be thrown but if it's mounted to a router, the error will be converted to a response object with the correct status code and headers.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    if(ctx.body.id === \"123\") {\n        throw ctx.error(\"Bad Request\", {\n            message: \"Id is not allowed\"\n        })\n    }\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n```\n\nYou can also instead throw using a status code:\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    if(ctx.body.id === \"123\") {\n        throw ctx.error(400, {\n            message: \"Id is not allowed\"\n        })\n    }\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n```\n\n### Endpoint\n\nEndpoints are building blocks of better-call. \n\n#### Path\n\nThe path is the URL path that the endpoint will respond to. It can be a direct path or a path with parameters and wildcards.\n\n```ts\n//direct path\nconst endpoint = createEndpoint(\"/item\", {\n    method: \"GET\",\n}, async (ctx) =\u003e {})\n\n//path with parameters\nconst endpoint = createEndpoint(\"/item/:id\", {\n    method: \"GET\",\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.params.id\n        }\n    }\n})\n\n//path with wildcards\nconst endpoint = createEndpoint(\"/item/**:name\", {\n    method: \"GET\",  \n}, async (ctx) =\u003e {\n    //the name will be the remaining path\n    ctx.params.name\n})\n```\n\n#### Body Schema\n\nThe `body` option accepts a standard schema and will validate the request body. If the request body doesn't match the schema, the endpoint will throw an error. If it's mounted to a router, it'll return a 400 error.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n```\n\n#### Query Schema\n\nThe `query` option accepts a standard schema and will validate the request query. If the request query doesn't match the schema, the endpoint will throw an error. If it's mounted to a router, it'll return a 400 error.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"GET\",\n    query: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.query.id\n        }\n    }\n})\n```\n\n#### Require Headers\n\nThe `requireHeaders` option is used to require the request to have headers. If the request doesn't have headers, the endpoint will throw an error. This is only useful when you call the endpoint as a function.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"GET\",\n    requireHeaders: true\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.headers.get(\"id\")\n        }\n    }\n})\ncreateItem({\n    headers: new Headers()\n})\n```\n\n#### Require Request\n\nThe `requireRequest` option is used to require the request to have a request object. If the request doesn't have a request object, the endpoint will throw an error. This is only useful when you call the endpoint as a function.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"GET\",\n    requireRequest: true\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.request.id\n        }\n    }\n})\n\ncreateItem({\n    request: new Request()\n})\n```\n\n### Handler\n\nthis is the function that will be invoked when the endpoint is called. It accepts a context object that contains the request, headers, body, query, params and other information. \n\nIt can return a response object, a string, a number, a boolean, an object or an array. \n\nIt can also throw an error and if it throws APIError, it will be converted to a response object with the correct status code and headers.\n\n- **Context**: the context object contains the request, headers, body, query, params and a helper function to set headers, cookies and get cookies. If there is a middleware, the context will be extended with the middleware context.\n\n### Middleware\n\nEndpoints can use middleware by passing the `use` option to the endpoint. To create a middleware, you can call `createMiddleware` and pass it a function or an options object and a handler function.\n\nIf you return a context object from the middleware, it will be available in the endpoint context.\n\n```ts\nimport { createMiddleware, createEndpoint } from \"better-call\";\n\nconst middleware = createMiddleware(async (ctx) =\u003e {\n    return {\n        name: \"hello\"\n    }\n})\n\nconst endpoint = createEndpoint(\"/\", {\n    method: \"GET\",\n    use: [middleware],\n}, async (ctx) =\u003e {\n   //this will be the context object returned by the middleware with the name property\n   ctx.context\n})\n```\n\n### Router\n\nYou can create a router by calling `createRouter` and passing it an array of endpoints. It returns a router object that has a `handler` method that can be used to serve the endpoints.\n\n```ts\nimport { createRouter } from \"better-call\"\nimport { createItem } from \"./item\"\n\nconst router = createRouter({\n    createItem\n})\n\nBun.serve({\n    fetch: router.handler\n})\n```\n\nBehind the scenes, the router uses [rou3](https://github.com/unjs/rou3) to match the endpoints and invoke the correct endpoint. You can look at the [rou3 documentation](https://github.com/unjs/rou3) for more information.\n\n#### Router Options\n\n**routerMiddleware:**\n\nA router middleware is similar to an endpoint middleware but it's applied to any path that matches the route. It's like any traditional middleware. You have to pass endpoints to the router middleware as an array.\n\n```ts\nconst routeMiddleware = createEndpoint(\"/api/**\", {\n    method: \"GET\",\n}, async (ctx) =\u003e {\n    return {\n        name: \"hello\"\n    }\n})\nconst router = createRouter({\n    createItem\n}, {\n    routerMiddleware: [{\n        path: \"/api/**\",\n        middleware:routeMiddleware\n    }]\n})\n```\n\n**basePath**: The base path for the router. All paths will be relative to this path.\n\n**onError**: The router will call this function if an error occurs in the middleware or the endpoint. This function receives the error as a parameter and can return different types of values:\n\n- If it returns a `Response` object, the router will use it as the HTTP response.\n- If it throws a new error, the router will handle it based on its type (if it's an `APIError`, it will be converted to a response; otherwise, it will be re-thrown).\n- If it returns nothing (void), the router will proceed with default error handling (checking `throwError` setting).\n\n```ts\nconst router = createRouter({\n    /**\n     * This error handler can be set as async function or not.\n     */\n    onError: async (error) =\u003e {\n        // Log the error\n        console.error(\"An error occurred:\", error);\n        \n        // Return a custom response\n        return new Response(JSON.stringify({ message: \"Something went wrong\" }), {\n            status: 500,\n            headers: { \"Content-Type\": \"application/json\" }\n        });\n    }\n});\n```\n\n**throwError**: If true, the router will throw an error if an error occurs in the middleware or the endpoint. If false (default), the router will handle errors internally. This setting is still relevant even when `onError` is provided, as it determines the behavior when:\n\n1. No `onError` handler is provided, or\n2. The `onError` handler returns void (doesn't return a Response or throw an error)\n\n- For `APIError` instances, it will convert them to appropriate HTTP responses.\n- For other errors, it will return a 500 Internal Server Error response.\n\n```ts\nconst router = createRouter({\n    throwError: true, // Errors will be propagated to higher-level handlers\n    onError: (error) =\u003e {\n        // Log the error but let throwError handle it\n        console.error(\"An error occurred:\", error);\n        // No return value, so throwError setting will determine behavior\n    }\n});\n```\n\n#### Node Adapter\n\nYou can use the node adapter to serve the router with node http server.\n\n```ts\nimport { createRouter } from \"better-call\";\nimport { toNodeHandler } from \"better-call/node\";\nimport { createItem } from \"./item\";\nimport http from \"http\";\n\nconst router = createRouter({\n    createItem\n})\nconst server = http.createServer(toNodeHandler(router.handler))\n```\n\n### RPC Client\n\nbetter-call comes with a rpc client that can be used to call endpoints from the client. The client wraps over better-fetch so you can pass any options that are supported by better-fetch.\n\n```ts\nimport { createClient } from \"better-call/client\";\nimport { router } from \"@serve/router\";\n\nconst client = createClient\u003ctypeof router\u003e({\n    /**\n     * if you add custom path like `http://\n     * localhost:3000/api` make sure to add the \n     * custom path on the router config as well.\n    */\n    baseURL: \"http://localhost:3000\"\n});\nconst items = await client(\"/item\", {\n    body: {\n        id: \"123\"\n    }\n});\n```\n\u003e You can also pass object that contains endpoints as a generic type to create client.\n\n### Headers and Cookies\n\nIf you return a response object from an endpoint, the headers and cookies will be set on the response object. But You can  set headers and cookies for the context object.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    ctx.setHeader(\"X-Custom-Header\", \"Hello World\")\n    ctx.setCookie(\"my-cookie\", \"hello world\")\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n```\n \nYou can also get cookies from the context object.\n\n```ts\nconst createItem = createEndpoint(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    const cookie = ctx.getCookie(\"my-cookie\")\n    return {\n        item: {\n            id: ctx.body.id\n        }\n    }\n})\n```\n\n\u003e other than normal cookies the ctx object also exposes signed cookies.\n\n### Endpoint Creator\n\nYou can create an endpoint creator by calling `createEndpoint.create` that will let you apply set of middlewares to all the endpoints created by the creator.\n\n```ts\nconst dbMiddleware = createMiddleware(async (ctx) =\u003e {\n   return {\n    db: new Database()\n   }\n})\nconst create = createEndpoint.create({\n    use: [dbMiddleware]\n})\n\nconst createItem = create(\"/item\", {\n    method: \"POST\",\n    body: z.object({\n        id: z.string()\n    })\n}, async (ctx) =\u003e {\n    await ctx.context.db.save(ctx.body)\n})\n```\n\n### Open API\n\nBetter Call by default generate open api schema for the endpoints and exposes it on `/api/reference` path using scalar. By default, if you're using `zod` it'll be able to generate `body` and `query` schema.\n\n```ts\nimport { createEndpoint, createRouter } from \"better-call\"\n\nconst createItem = createEndpoint(\"/item/:id\", {\n    method: \"GET\",\n    query: z.object({\n        id: z.string({\n            description: \"The id of the item\"\n        })\n    })\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.query.id\n        }\n    }\n})\n```\n\nBut you can also define custom schema for the open api schema.\n\n```ts\nimport { createEndpoint, createRouter } from \"better-call\"\n\nconst createItem = createEndpoint(\"/item/:id\", {\n    method: \"GET\",\n    query: z.object({\n        id: z.string({\n            description: \"The id of the item\"\n        })\n    }),\n    metadata: {\n    openapi: {\n        requestBody: {\n            content: {\n                \"application/json\": {\n                    schema: {\n                        type: \"object\",\n                        properties: {\n                            id: {\n                                type: \"string\",\n                                description: \"The id of the item\"\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n   }\n}, async (ctx) =\u003e {\n    return {\n        item: {\n            id: ctx.query.id\n        }\n    }\n})\n```\n\n#### Configuration\n\nYou can configure the open api schema by passing the `openapi` option to the router.\n\n```ts\nconst router = createRouter({\n    createItem\n}, {\n    openapi: {\n        disabled: false, //default false\n        path: \"/api/reference\", //default /api/reference\n        scalar: {\n            title: \"My API\",\n            version: \"1.0.0\",\n            description: \"My API Description\",\n            theme: \"dark\" //default saturn\n        }\n    }\n})\n```\n\n## License\nMIT","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBekacru%2Fbetter-call","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FBekacru%2Fbetter-call","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBekacru%2Fbetter-call/lists"}