{"id":15493450,"url":"https://github.com/sinclairzx81/servicebox","last_synced_at":"2025-04-22T19:49:41.265Z","repository":{"id":57143880,"uuid":"344916832","full_name":"sinclairzx81/servicebox","owner":"sinclairzx81","description":"Typed Web Services for NodeJS","archived":false,"fork":false,"pushed_at":"2022-03-07T06:18:54.000Z","size":389,"stargazers_count":22,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-22T13:01:32.288Z","etag":null,"topics":["json-rpc","json-schema","runtime-typechecking"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sinclairzx81.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":"2021-03-05T19:45:09.000Z","updated_at":"2025-02-11T21:57:32.000Z","dependencies_parsed_at":"2022-09-06T00:11:41.678Z","dependency_job_id":null,"html_url":"https://github.com/sinclairzx81/servicebox","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairzx81%2Fservicebox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairzx81%2Fservicebox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairzx81%2Fservicebox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairzx81%2Fservicebox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sinclairzx81","download_url":"https://codeload.github.com/sinclairzx81/servicebox/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250313266,"owners_count":21410192,"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":["json-rpc","json-schema","runtime-typechecking"],"created_at":"2024-10-02T08:06:47.105Z","updated_at":"2025-04-22T19:49:41.228Z","avatar_url":"https://github.com/sinclairzx81.png","language":"TypeScript","readme":"\u003cdiv align='center'\u003e\n\n\u003ch1\u003eServiceBox\u003c/h1\u003e\n\n\u003cp\u003eTyped Web Services for NodeJS\u003c/p\u003e\n\n[![npm version](https://badge.fury.io/js/%40sinclair%2Fservicebox.svg)](https://badge.fury.io/js/%40sinclair%2Fservicebox) [![GitHub CI](https://github.com/sinclairzx81/servicebox/workflows/GitHub%20CI/badge.svg)](https://github.com/sinclairzx81/servicebox/actions)\n\n\u003c/div\u003e\n\n```typescript\nimport { Service, Method, Type } from '@sinclair/servicebox'\n\n// -------------------------------------\n// Service\n// -------------------------------------\n\nconst service = new Service({ \n    'add': new Method([], {\n        request: Type.Tuple([\n            Type.Number(), \n            Type.Number()\n        ]),\n        response: Type.Number()\n    }, (context, [a, b]) =\u003e {\n        return a + b\n    })\n})\n\n// -------------------------------------\n// Test\n// -------------------------------------\n\nconst result = await service.execute('add', {}, [1, 2])\n\nassert(result, 3)\n\n// -------------------------------------\n// Host\n// -------------------------------------\n\nconst app = express()\n\napp.post('/api', (req, res) =\u003e service.request(req, res))\n\napp.listen(5000)\n\n// -------------------------------------\n// Call\n// -------------------------------------\n\nconst results = await post('http://localhost:5000/api', [\n    { jsonrpc: '2.0', method: 'add', params: [10, 20] },\n    { jsonrpc: '2.0', method: 'add', params: [20, 30] },\n    { jsonrpc: '2.0', method: 'add', params: [30, 40] }\n])\n\n// results = [\n//   { jsonrpc: '2.0', id: null, result: 30 },\n//   { jsonrpc: '2.0', id: null, result: 50 },\n//   { jsonrpc: '2.0', id: null, result: 70 },\n// ]\n\n```\n## Overview\n\nServiceBox is a library for building type safe Web Services in NodeJS. It offers a set of web service types that are used to compose methods whose requests are runtime checked with [JSON Schema](https://json-schema.org/) and statically checked with TypeScript. ServiceBox is designed to allow for documentation and validation logic to be derived from a runtime type system based on JSON schema. This library can be used independently or integrated into existing applications via middleware.\n\nBuilt with TypeScript 4.2 and Node 14 LTS.\n\nLicense MIT\n\n## Install\n\n```bash\n$ npm install @sinclair/servicebox\n```\n\n## Contents\n\n- [Overview](#Overview)\n- [Methods](#Methods)\n- [Middleware](#Middleware)\n- [Contracts](#Contracts)\n- [Exceptions](#Exceptions)\n- [Services](#Services)\n- [Testing](#Testing)\n- [Protocol](#Protocol)\n- [Metadata](#Metadata)\n\n## Methods\nServiceBox methods are created using the following parameters. See sections below for more details.\n\n```typescript\nconst method = new Method([...middleware], contract, body)\n```\n## Middleware\n\nServiceBox middleware are implementations of `Middleware\u003cContext\u003e` that map `IncomingMessage` requests to context objects that are passed to the methods `context` argument. Middleware functions can be used for reading header information from an incoming request and preparing a valid context for the method to execute on. Middleware can also be used to reject a request (for example failed Authorization checks). For more information on rejecting requests see the [Exceptions](#Exceptions) section.\n\nMethods can apply multiple middleware which will be merged into the methods `context` argument. If a middleware should not return a context, the middleware should return `null`.\n\n```typescript\nimport { IncomingMessage } from 'http'\n\nexport class Foo {\n    map(request: IncomingMessage) { \n        return { foo: 'foo' }\n    }\n}\n\nexport class Bar {\n    map(request: IncomingMessage) { \n        return { bar: 'bar' }\n    }\n}\nexport class Baz {\n    map(request: IncomingMessage) { \n        return null // no context\n    }\n}\n\nconst method = new Method([\n    new Foo(), \n    new Bar(), \n    new Baz()\n] {\n    request: Type.Any(),\n    response: Type.Any()    \n}, ({ foo, bar }, request) =\u003e {\n    //\n    // ^ from middleware\n    //\n})\n```\n\n## Contracts\n\nContracts describe the `Request` and `Response` signature for a method and are represented internally as JSON Schema. ServiceBox is able to resolve the appropriate TypeScript static types for the request and response using type inference. For more information on the `Type` object refer to the [TypeBox](https://github.com/sinclairzx81/typebox) project.\n\n```typescript\nimport { Method, Type } from '@sinclair/servicebox'\n \n// type Add = (request: [number, number]) =\u003e number\n\nconst add = new Method([], {\n    request:  Type.Tuple([ \n        Type.Number(), \n        Type.Number() \n    ]),\n    response: Type.Number()\n}, (context, [a, b]) =\u003e {\n    //\n    // [a, b] = [number, number]\n    //\n    return a + b // number\n})\n```\n\n## Exceptions\n\nBy default, errors that are thrown inside a method or middleware will cause the method to respond with a non-descriptive error message. It is possible to override this and return application specific error codes and messages by throwing instances of type `Exception`. The example below creates a `NotImplementedException` by extending the type `Exception`.\n\n```typescript\nimport { Method, Type, Exception } from '@sinclair/servicebox'\n\n\nexport class NotImplementedException extends Exception {\n    constructor() {\n        super(4000, \"Method not implemented\")\n    }\n}\n\nconst add = new Method([], {\n    request: Type.Tuple([ \n        Type.Number(), \n        Type.Number() \n    ]),\n    response: Type.Number()\n}, (context, request) =\u003e {\n    throw new NotImplementedException()\n})\n\n// Which results in the following error.\n//\n// { \n//    error: { \n//      code: 4000, \n//      message: 'Method not implemented', \n//      data: {}\n//    } \n// }\n```\n\n## Services\n\nServices are containers for methods. Services handle method routing logic as well as invoking calls on the requested method. The following example creates a service on the `/api` route using `express`. \n\n```typescript\nimport { Service, Method, Type } from '@sinclair/servicebox'\n\nconst service = new Service({\n    'add': new Method([], {\n        request: Type.Tuple([\n            Type.Number(), \n            Type.Number()\n        ]),\n        response: Type.Number()\n    }, (context, [a, b]) =\u003e {\n        return a + b\n    })\n})\n\n\n// ------------------------------------------\n// Bind the service to the /api route.\n// ------------------------------------------\n\nconst app = express()\n\napp.use('/api', (req, res) =\u003e {\n    service.request(req, res))\n})\n\napp.listen(5000)\n```\n## Testing\n\nYou can run any method created on the service using the services `execute()` function. Calls to `execute()` will bypass HTTP and invoke the method directly. As such the `context` object should match that of any middleware used by the method. As follows.\n\n```typescript\nexport class Foo {\n    map() {\n        return { foo: 1 }\n    }\n}\n\nconst service = new Service({\n    'add': new Method([new Foo()], {\n        request: Type.Tuple([\n            Type.Number(), \n            Type.Number()\n        ]),\n        response: Type.Number()\n    }, (context, [a, b]) =\u003e {\n        return a + b\n    })\n})\n\n// -----------------------------------------\n// Tests\n// -----------------------------------------\n\nconst response = await service.execute('add', { \n    foo: 1 \n}, [1, 2])\n\nassert(response, 3)\n```\n\n## Protocol\n\nServiceBox implements the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification. Requests to invoke a method must be sent via HTTP `POST` passing the `{ 'Content-Type': 'application/json' }` header and request payload. ServiceBox accepts requests as batched JSON-RPC requests.\n\nSee the `example/client.ts` class that provides a basic implementation for a client.\n\n```typescript\n\n// -------------------------------------\n// Service\n// -------------------------------------\n\nconst service = new Service({\n    \"add\": new Method([], {\n        request: Type.Tuple([\n            Type.Number(),\n            Type.Number()\n        ]),\n        response: Type.Number()\n    }, (context, [a, b] =\u003e a + b))\n})\n\n// ------------------------------------\n// Request\n// ------------------------------------\n\nconst result = await post(endpoint, [\n    { jsonrpc: '2.0', method: 'add', params: [10, 20] },\n    { jsonrpc: '2.0', method: 'add', params: [20, 30] },\n    { jsonrpc: '2.0', method: 'add', params: [30, 40] }\n])\n\n// result = [\n//   { jsonrpc: '2.0', id: null, result: 30 },\n//   { jsonrpc: '2.0', id: null, result: 50 },\n//   { jsonrpc: '2.0', id: null, result: 70 },\n// ]\n```\n\n## Metadata\n\nMetadata for a service can be obtained in two ways. The first is inspecting the `service.metadata` property. The other is making a `HTTP GET` request to the services HTTP endpoint. The following inspects the metadata using `service.metadata`.\n\n\u003e Note: ServiceBox will respond with metadata for the service if it receives a `HTTP GET` request. To disable this, ensure that the `service.request(req, res)` is called only for `HTTP POST` requests.\n\n```typescript\nimport { Service, Method, Type } from '@sinclair/servicebox'\n\nconst service = new Service({\n    'add': new Method([], {\n        request: Type.Tuple([\n            Type.Number(),\n            Type.Number()\n        ]),\n        response: Type.Number()\n    }, (context, [a, b]) =\u003e {\n        return a + b\n    })\n})\n\nconsole.log(service.metadata)\n\n// service.metadata = {\n//     \"add\": {\n//         \"request\": {\n//             \"type\": \"array\",\n//             \"items\": [\n//                 { \"type\": \"number\" },\n//                 { \"type\": \"number\" }\n//             ],\n//             \"additionalItems\": false,\n//             \"minItems\": 2,\n//             \"maxItems\": 2\n//         },\n//         \"response\": {\n//             \"type\": \"number\"\n//         }\n//     }\n// }\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairzx81%2Fservicebox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsinclairzx81%2Fservicebox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairzx81%2Fservicebox/lists"}