Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

JavaScript as a Query Language: The Invisible RPC

Posted on Oct 23 • Originally published at blog.doseofted.me Getting data from a JavaScript Server is too complicated. I'm not saying that it's difficult but complicated. We have many options to do so but they all require us to rethink how we acquire that data. Let's say that I have some Function defined on the server:This function does exactly what I want. I haven't yet thought about the server, I just know that I want to create a profile and get the ID back. Let's consider how this function gets to the client.We could set up an HTTP server but we'll need to wrap our function in a route, mapping our function to HTTP-specific verbs and errors. We could try GraphQL but we'll need to define a schema for our function in its IDL. We could try gRPC but we'll need to create protocol buffers, implement them, and generate a client. We could try tRPC but we'll need to wrap our function in its procedures and router. But even then, most of these options require special processing for files or additional setup for a type-safe client or a lot more work to support real-time events from the server.None of these options are necessarily difficult and there are additional frameworks to make them easier but they all add a lot of wrappers to the function that I want to call when I just want to call the function itself. And this is just one function, what if I have hundreds?If I have a server written in JavaScript and the client is JavaScript, shouldn’t I be able to call the function, like it’s JavaScript?That's what we're going to do today. We'll use an open-source library called Prim+RPC: a thin layer between the server and client of your choice that translates function calls and results so that you can simply call your server functions on the client as intended.💡 This article is a much shorter adaptation of the setup guide on the Prim+RPC website. If you'd like a more in-depth look at this project, check out the full guide.Star on GitHubI've set up a starter project for us, so we don't have to start from scratch. It is a monorepo composed of two modules: a Node server where our function will be defined and a client that will call the function, both serialized using Prim+RPC's plugins for the Fetch API.You can download the project with the command below or download it directly:It's also available and easy to get started on StackBlitz:You can start the project by running npm install to install dependencies and then npm run dev to start both the server and client parts of the project. The client will be available at http://localhost:3000.We'll be greeted with a message "Not implemented". Let's fix that!Let's set up the server first. First things first: let's define a function. We'll work with something simple to get started. Add this function to server/module.ts:This is just a regular JavaScript function. The only thing that we added is a property .rpc that signals to Prim+RPC that we want to expose this function as RPC. Since Functions in JavaScript are also Objects, this is valid syntax (we could also omit this property and add our function to an allow-list).Moving on to the server framework itself, replace the contents of server/index.ts with the following. I'll explain what this does:First, we pass the module containing our function to createPrimServer(): a framework-agnostic utility to receive RPC and respond with RPC results. We then pass the Prim+RPC server to a method handler, primFetch(), which wraps our RPC into Fetch API Request and Response objects. We also add two necessary CORS headers on every Request so that our client in the browser is allowed to request resources from the server.💡 If you are running this example from Stackblitz, you may need to change the access-control-allow-origin header value to https://localhost:3000 if you experience any issues running the project (using secure HTTP).The returned fetch utility is a function that takes a Request and returns a Response, as expected by server frameworks like Deno and Bun. Since Node doesn't yet support this natively, we use an adapter, createServerAdapter(), for Node's createServer utility. Now we can expose our function over HTTP, using port 3000. As a last step, we export the type of our module which will later become a type import on the client (exposing types only, not the actual code).We did it. We're ready to move on to the client. As a sanity check, we can test this out now by sending a simple request. With the server running, try out this command :Our Prim+RPC server is now available at http://localhost:3000/prim (you can also change the path prefix if you'd like).The client is very easy to set up. Replace the contents of client/prim.ts with the following. I'll explain what this does:We have set up the Prim+RPC client, a framework-agnostic tool for sending RPC and receiving RPC results. We pass a type parameter to our client with the type of our module: the Prim+RPC client will apply RPC-specific transformations to our types. Note that we don't pass the module code itself, only the TypeScript types. We then pass the server address to the client as well as a method plugin that will turn our function calls into Requests for the Fetch API. We export this client to be used throughout our app.That's all there is to it. We can now use this client to call our function. Replace the contents of client/index.ts with the following:We didn't need to generate code for a client or wrap our function call in some request specific to the client, we just called the function and received a result.Since our server and client are in two separate environments and it may take a (very short) amount of time to resolve, our result is wrapped in a Promise so as not to block execution on the main thread (which means we await the result). But the Prim+RPC client automatically transforms our Module types for us, so we know.Now, we can add more functions to the server and we don't need any more setup to call those functions on the client. It's just ready to be called.We could stop here. But we can do much more with Prim+RPC: we can support file uploads and downloads, additional types, add validation, and even pass server context to our functions when needed.Read on to learn how we can use these features.⭐ Prim+RPC is an open-source project, currently in prerelease. If you find the library useful, consider giving it a star on GitHub and sharing it with others.Star on GitHubWhen calling a function over the Internet, we don't know what could be passed to the server. Prim+RPC doesn't validate arguments by default but we can add this easily. Let's validate using Zod as an example. First, we'll install it on the server:Let's modify our server/module.ts so we validate and overwrite the given arguments:And now we have validation. If you'd like to ensure functions are never defined without validation, check out Prim+RPC's security guide for more details.Prim+RPC can support files as arguments and return them to the caller. With the Fetch plugin, we don't even have to do any extra work. It works out of the box! Let's demonstrate this with an example that converts a Markdown file to HTML, using micromark. First, we'll install it on the server:And replace server/module.ts with the following:On the client, we just call it. Update client/index.ts:We could also access returned files from a URL, for easy file downloads. Try the following command or visit the given URL in your web browser:Prim+RPC, by default, can support arguments and return values serializable by JSON (with files processed separately). But what if we want more? Libraries like superjson can add support for additional types like Map, Set, Date, BigInt, and more. And Prim+RPC supports custom serialization like superjson! Let's install it in our project:Now we can change the .jsonHandler option in server/index.ts. Replace the contents of this file with the following:Serialization and deserialization go hand-in-hand so let's add this to the client too. Replace the contents of client/prim.ts with the following:Now can create functions that use these new supported types. Let's try it out. Replace server/module.ts with:And call it in client/index.ts:See the JSON Handler documentation to learn how to set up other popular JSON alternatives.Prim+RPC can support callbacks on your function as well. We just need to set up a callback handler. While functions return once, callbacks on a function can fire multiple times so we need to maintain a connection to the server. For this, we'll use a callback handler backed by a WebSocket connection.First, let's install ws which will act as our WebSocket server:Replace server/index.ts with the following. And stick with me: this is one-time setup and we don't have to touch WebSockets again after this:Are you still with me? We have just set up a WebSocket connection and passed that WebSocket server to Prim+RPC. Now the server can handle callbacks on your functions!However, the client doesn't yet know how to tell the server that it has callbacks. Let's do that now using the WebSocket callback plugin:And now we can use callbacks on our functions. Let's create a function that uses them in server/module.ts:This will type the message that we give it, one-by-one. Not a great use of server resources but a good demo. Let's try this on the client (client/index.ts):And now we see the message typed out on the page!Up to this point, we have not really touched the server inside of our functions. Instead, we've passed everything that our function needs as arguments. However, we can't always just ignore the server: it contains information that we may need that doesn't have direct comparisons to calling a function in JavaScript. But we can still access the server context from our functions. And that's what we're going to do next.In this example, we’ll set a secret cookie from the client that is required to access our secret function. Without the cookie: no function access. However, our function won’t have to touch the cookie at all.First, let's install a tiny helper package:And now we can use this package on our server. We'll define a contextTransform option that will be given the Request object (from the Fetch API) for every function call that we make to the server. The returned value from this function will become the context (this value) of our function.Replace server/index.ts with the following:💡 If you are running this example from Stackblitz, you will need to change the access-control-allow-origin header value to https://localhost:3000 for this example to work in the browser (using secure HTTP).Inside of the contextTransform function, we return setSecret() and allowed which will be used from our function. We have also updated the CORS headers to allow credentials so that the browser can utilize the cookies we set. Using TypeScript, we get that return type and assign it a type ServerContext. We'll pass this as a type hint to our function.❗ This is of course a demo. In a real application, you will want to use some form of cryptography.Replace server/module.ts with the following:Notice that we only accessed the functions that we defined in the server's context so we didn't even have to touch the cookie at all. The this parameter is only a type hint and is not an argument that we have to pass: Prim+RPC will handle that for us.Now we can make one small change to the client. Since we're passing cookies down to the client, we must set the credentials option of the fetch function to “include” so that cookies can be set properly.Let's do that real quick in client/prim.ts:And now we can call our function in client/index.ts:Since we have set the secret in a cookie, we don't technically need to set the cookie anymore. If we just comment out that line:You'll find that we can still see the secret because we had already set the secret before and no longer need to pass any arguments to our function. It's handled in the server context!This can be a powerful tool for setting up authentication, adding redirects, or otherwise integrating with the server of your choice.All of the frameworks that I mentioned in the beginning have their place. But when I'm getting started on a new project, how messages get passed from server to client is far from the first thing on my mind. It's important but it shouldn't dictate the logic happening on my server or restrict what I want to do.Prim+RPC provides a powerful library for calling functions remotely and remains invisible once it is set up. I can define a function on the server of my choice and call that function on the client, all using JavaScript. Yet requests are simple enough to make outside of JavaScript as well.⭐ Thank you for following along with me. If you like this method of retrieving data from the server, consider giving Prim+RPC a star on GitHub.Star on GitHubTry adding your own functions to this project, maybe build out a simple API. If you get stuck, there are several examples on the Prim+RPC website as well as this post.Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well Confirm For further actions, you may consider blocking this person and/or reporting abuse Dmitry A. Efimenko - Oct 16 Alvaro Montoro - Oct 16 Andrew Ezeani - Sep 24 keploy - Sep 23 Once suspended, doseofted will not be able to comment or publish posts until their suspension is removed. Once unsuspended, doseofted will be able to comment and publish posts again. Once unpublished, all posts by doseofted will become hidden and only accessible to themselves. If doseofted is not suspended, they can still re-publish their posts from their dashboard. Note: Once unpublished, this post will become invisible to the public and only accessible to Ted Klingenberg. They can still re-publish the post if they are not suspended. Thanks for keeping DEV Community safe. Here is what you can do to flag doseofted: doseofted consistently posts content that violates DEV Community's code of conduct because it is harassing, offensive or spammy. Unflagging doseofted will restore default visibility to their posts. DEV Community — A constructive and inclusive social network for software developers. With you every step of your journey. Built on Forem — the open source software that powers DEV and other inclusive communities.Made with love and Ruby on Rails. DEV Community © 2016 - 2023. We're a place where coders share, stay up-to-date and grow their careers.



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

JavaScript as a Query Language: The Invisible RPC

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×