{"id":26886413,"url":"https://github.com/vepeckman/nerve-rpc","last_synced_at":"2025-05-08T23:11:50.624Z","repository":{"id":93750218,"uuid":"167858357","full_name":"vepeckman/nerve-rpc","owner":"vepeckman","description":"Nim RPC framework","archived":false,"fork":false,"pushed_at":"2019-11-27T02:47:32.000Z","size":147,"stargazers_count":38,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-05-08T23:11:44.563Z","etag":null,"topics":["nim","rpc","rpc-framework","web"],"latest_commit_sha":null,"homepage":null,"language":"Nim","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/vepeckman.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-01-27T21:02:35.000Z","updated_at":"2024-05-29T17:43:41.000Z","dependencies_parsed_at":null,"dependency_job_id":"d0175c02-2ce5-4bd7-b0e9-711da78a4797","html_url":"https://github.com/vepeckman/nerve-rpc","commit_stats":null,"previous_names":["vepeckman/nerve-rpc"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vepeckman%2Fnerve-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vepeckman%2Fnerve-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vepeckman%2Fnerve-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vepeckman%2Fnerve-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vepeckman","download_url":"https://codeload.github.com/vepeckman/nerve-rpc/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253160779,"owners_count":21863631,"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":["nim","rpc","rpc-framework","web"],"created_at":"2025-03-31T19:18:44.265Z","updated_at":"2025-05-08T23:11:50.600Z","avatar_url":"https://github.com/vepeckman.png","language":"Nim","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nerve RPC\n\nNerve is a RPC framework for building APIs in Nim. It prioritizes flexibility, ease of use, and performance. Nerve provides a compile time macro that generates both an efficient router for dispatching RPC requests on the server, as well as a complete, fully typed, client for both native and JavaScript targets.\n\n### Install\n\nNerve is available on Nim's builtin package manager, [nimble](https://github.com/nim-lang/nimble).\n\n`nimble install nerve`\n\n### Goals:\n\n- Reduce the incidental complexity around declaring and calling remote procedures. Declaring remote procedures should be as simple as declaring local procedures, and calling them should be as simple as calling local procedures.\n- Be fast. Nim generates performant native binaries, and Nerve aims to utilize that speed.\n- Have a low cognitive overhead. Nerve does most of the heavy lifting with one macro, supported by a handful of utilities.\n\n### Non-goals:\n\n- Be a general purpose RPC server or client. Nerve implements JSON RPC, so external clients can be written. But it is designed to be used with the built in client, and ease of use for that client is top priority.\n\n# Hello World\n\nThe following `main.nim` is a Nerve server, native client, and Javascript client. Compile and run the server with `nim c -r -d:nerveServer main.nim`. In a seperate tab, compile and run the native client with `nim c -r -d:nerveClient main.nim`. Compile the Javascript client with `nim js -d:nerveClient main.nim` and open `localhost:1234` in a browser to view the browser console.\n\n```nim\n# main.nim\nimport nerve\n\n# Declare the service with Nerve's service macro, provide an identifier and uri\nservice HelloService, \"/api\":\n\n  # Declare procs for the service using Nim's normal proc definitions\n  proc greet(name = \"world\"): Future[string] = futureWrap(\"Hello \" \u0026 name)\n\n  proc runTask(task: string): Future[void] =\n    echo \"Running task \" \u0026 task\n    result = voidFuture()\n\n\n  # Modifier macro to setup the http server\n  server:\n\n    # Import and setup Nim's built in http server\n    import asyncHttpServer\n    let server = newAsyncHttpServer()\n\n    # Create a RPC server for the declared Nerve service\n    let helloServer = HelloService.newServer()\n\n    # Handler for the http server\n    proc cb (req: Request) {.async, gcsafe.} =\n      case req.url.path\n      of HelloService.rpcUri:\n        # If a request has the service uri, dispatch the request to the service\n        await req.respond(Http200, $ await helloServer.routeRpc(req.body))\n      of \"/client.js\":\n        # JavaScript file for the frontend\n        let headers = newHttpHeaders()\n        headers[\"Content-Type\"] = \"application/javascript\"\n        await req.respond(Http200, readFile(\"main.js\"), headers)\n      of \"/\":\n        # HTML file for the frontend\n        await req.respond(Http200, \"\"\"\u003chtml\u003e\u003chead\u003e\u003cmeta charset=\"UTF-8\"\u003e\u003c/head\u003e\u003cbody\u003eTesting\u003c/body\u003e\u003cscript src=\"client.js\"\u003e\u003c/script\u003e\u003c/html\u003e\"\"\")\n      else:\n        await req.respond(Http404, \"Not Found\")\n\n    waitFor server.serve(Port(1234), cb)\n\n\n  # Modifier macro to setup the http client\n  client:\n    const host = if defined(js): \"\" else: \"http://127.0.0.1:1234\"\n\n    proc main() {.async.} =\n      # Create a RPC client for the declared Nerve service\n      let helloClient = HelloService.newHttpClient(host)\n\n      # Use the remote methods defined on the service\n\n      echo await helloClient.greet(\"Nerve\") # Prints \"Hello Nerve\" to the console\n      await helloClient.runTask(\"serverside_task\") # Prints \"Running task serverside_task\" on the server\n\n    when defined(js):\n      discard main()\n    else:\n      waitFor main()\n```\n\n# Overview\n\nThe majority of Nerve's functionality is provided by the main `nerve` module.\n\n## `service` macro\n\n```nim\nmacro service*(name: untyped, uri: untyped = nil, body: untyped = nil): untyped\n```\n\nNerve's `service` macro is responsible for doing all of the setup and code generation required for RPC services. It takes an identifier, an optional uri, and a list of normal Nim procedures as its body. It produces an RpcService (accessible via the identifier) that can be instantiated into either a client or a server object with fields for each of the provided procs. The macro generates an object type that extends the `RPCServerInst` type provided by Nerve that describes instances of service clients and service servers. The macro also generates a dispatch function that dispatches incoming requests to the correct proc on the service. The provided procedures must have a return type of `Future[T]`, as the client will always use these functions asynchronusly.\n\n\n\nBy default, the `service` macro produces both the client and server code for each service. Nerve provides serveral methods for controlling what code is generated (see configuration), which could be desirable if proc implementations contain code specific to the server target. As file with the `service` macro can be compiled for both native and JS targets, those files should focus _only_ on the API functionality. Be aware that any types used and any modules imported by the API files also should to be accessible on both targets. The `serverImports` macro modifier can be used to import certain modules only on the server, which is useful for any proc implementation containing server specific code.\n\n## `newServer` macro\n\n```nim\nmacro newServer*(rpc: static[RpcService], injections: varargs[untyped]): untyped\n```\n\nThe `newServer` macro takes a service defined with `service` and instantiates a RPC server. The created service instance can then use `routeRpc` to take a `string` or `JsonNode`, dispatch the request, and return a response. The server instance provides only this dispatch functionality; it does not listen to any ports or otherwise connect to the network. The user must setup their HTTP server (or server for any other protocol) and then call the RPC server when a request comes that should be handled by the RPC server. The `newServer` macro optionally takes injected variables, see the `inject` modifier for more information.\n\n## `newClient` and `newHttpClient` macros\n\n```nim\nmacro newClient*(rpc: static[RpcService], driver: NerveDriver): untyped\nmacro newHttpClient*(rpc: static[RpcService], host: static[string] = \"\"): untyped\n```\n\nThe `newClient` macro takes a service defined with `service` and a driver, and instantiates a RPC client. The driver is a function responsible for making the requests to the server and returning the response. Drivers can be found in the `nerve/driver` module, or user defined. The `newHttpClient` macro combines the `newClient` macro with the an HTTP driver for convenience.\n\n## `serverImport` modifier\n\n```nim\nmacro serverImport*(imports: untyped)\n```\n\nIn the web domain, its likely that servers will contain server specific code from modules that interact with databases, other servers, or filesystems. The `serverImport` modifier gives the `service` macro the import modules only when the service is configured as a server.\n\n```nim\nservice FileService, \"/api/file\":\n    serverImport(os)\n    \n    proc save(filename, text: string): Future[void] =\n        # Use procs from the os module here\n```\n\n## `inject` modifier\n\n```nim\nmacro inject*(injections: untyped)\n```\n\n\n\nAll of the parameters for the RPC procedures must come from the client. However, Nerve provides a method for injecting variables from the server (such as client connection references, a service client, or anything that doesn't serialize well). To define variables for injection, place an `inject` statement in the service declaration. In the inject statement, include `var` definitions for the desired variables. These variable can then be used in any of the RPC procs. The actual injection is done in the `newServer` constructor, where the injected variables are provided to the server.\n\n```nim\nservice GreetingService, \"/api/greeting\":\n    inject:\n        var \n          id = 100\n          count: int\n        var uuid = \"asdf\"\n        \n    proc greet(greeting = \"Hello\", name = \"World\"): Future[string] =\n        echo uuid\n        futureWrap(greeting \u0026 \" \" \u0026 name)\n\nlet server = GreetingService.newServer(count = 1, uuid = \"fdsa\")\n```\n\n## `server` and `client` modifiers\n\n```nim\nmacro server*(serverStmts: untyped)\nmacro client*(clientStmts: untyped)\n```\n\nThe RPC clients and servers can be setup anywhere in the codebase, and can even be instantiated multiple times. However, it might be convenient to initialize a single instance of a client and server in the same file as the service declaration. The `server` and `client` modifiers enable this functionality. Each takes a code block, and executes that codeblock if the services is configured as a server or client, respectively (both will be executed if the service is configured to be both a server and a client). These macros can also be used to setup all the server and client code (as in the Hello World example), though this is only recommended for simple server/client setups.\n\n```nim\nservice GreetingService, \"/api/greeting\":\n    proc greet(greeting = \"Hello\", name = \"World\"): Future[string] =\n     futureWrap(greeting \u0026 \" \" \u0026 name)\n     \n    server:\n        let greetingServer* = GreetingService.newServer()\n        \n    client:\n        let greetingClient* = GreetingService.newHttpClient()\n```\n\n## nerve/drivers\n\n```nim\ntype NerveDriver* = proc (req: JsonNode): Future[JsonNode] {.gcsafe.}\n```\n\nNerve uses drivers to power its clients. The driver recieves a completed JSON RPC request, and is responsible for sending that to the server and returning the JSON RPC response. The `nerve/drivers` module provides common drivers (such as an http driver), but user defined drivers can be used as well. The `nerve` module exports the `nerve/drivers` modules, so it is not necessary to import `drivers` separately.\n\n## nerve/promises\n\nThe `promises` module provides target neutral (importable for both JS and native compiles) access to `Future[T]` types, as well as some helper functions. It is imported automatically when the `service` macro runs, and is accessible from any of the service procs or modifiers.\n\n## nerve/websockets\n\nNerve has experimental support for Websockets as transport layer. It includes a websocket type built on top of treeform's [ws](https://github.com/treeform/ws) library on native clients, and the default built in browser implementation for JavaScript. Checkout Nerve's test suites for usage of the provided websocket driver and message callbacks.\n\n# Configuration\n\nThe default behavior of the `service` macro is to produce both client and server code, but Nerve provides several options to configure this. The `nerve` module contains a `setDefaultConfig` macro that change the default behvior to produce either a server or a client. The `setDefaultConfig` macro takes a `ServiceConfigKind`: an enum that describes the different config options. The default can also be changed by defining the symbol `nerveClient` or `nerveServer` with the `-d` nim compiler flag. The final and most granular method of configuration is the `configureNerve` macro. The `configureNerve` macro takes a table of service identifiers to `ServiceConfigKind`. Important note: `configureNerve` and `setDefaultConfig` must run before the service module to correctly instantiates it. This means the macro call must run before the import of the service module.\n\n```nim\nsetDefaultConfig(sckServer)\nconfigureNerve({\n    GrettingService: sckClient,\n    FileService: sckServer\n})\n```\n\n# Errors\n\nErrors in RPC calls are propogated to the client. The client code will throw an `RpcError` with information from the error thrown on the server. If the server responds with a non-200 error code, the client throws an `InvalidResponseError`. The server throws errors for incorrect requests, per the JSON-RPC spec.\n\n# Gotchas\n\nNerve trys to be as low friction as possible. However there are a couple edges to watch for.\n\n1) Procedures under the same RPC server must have different names. No static method dispatch is possible.\n2) Generic procs are also not possible.\n3) The `service` macro doesn't mesh well with the Nim's `async` macro. See the section on promises for work arounds and more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvepeckman%2Fnerve-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvepeckman%2Fnerve-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvepeckman%2Fnerve-rpc/lists"}