How to achieve type safety from client to server

Sandro Maglione

Sandro Maglione

Web development

I am on a journey to achieve full type safety frontend to backend πŸšΆπŸ»β€βž‘οΈ

After bumping into some issues with server actions I discovered (effect) rpc.

And it was 🀯

Here is the full story and all the pieces of my type-safe puzzle 🧩

Frontend is becoming more backend-ish

Frontend used to be about styles, components, css and these kind of stuff πŸ’πŸΌβ€β™‚οΈ

Checklist before releasing that new frontend feature of your app βœ… Accessibility βœ… Responsiveness βœ… Analytics events βœ… Security βœ… Error reporting βœ… Bundle size βœ… Performance Well, frontend is complex indeed πŸ€”


But then everyone realized that data must come from somewhere, and it's the frontend that is responsible to request it.

Result: Promise, Suspense, error handling, loading state, and more and more.

We are about to reach a new dimension with React 19: Server components and Server actions ("server" you see πŸ˜…).

Frontend is not only "front" anymore, it's time to upgrade your skills to "back" as well.

The convenience and cost of server actions

Server actions come to the rescue: execute server code as simple as a function call πŸͺ„

Catchy promise, convenient, but with some limitations, some of which break my type-safe model:

  • No error handling whatsoever
  • Serialize/Deserialize is a pain

Error handling in server actions has a large room for improvements πŸ€” When an action fails it always returns 500 error, which doesn’t help much with providing a clear error message on the client πŸ‘€ Am I missing something?


How server actions work under the hood

Back to the drawing board then πŸ§‘β€πŸ«

How can I have the convenience of server actions (single function call) with full type safety?

Behind "a single function call" there are some layers that execute some magic:

  • Serialize the function parameters
  • Perform an http request to the server passing the parameters in the body
  • Deserialize (validate/parse) the parameters on the server
  • Perform the request on the server
  • Serialize and return the response to the client
  • Deserialize the response on the client and provide it as the return of the function called initially

From client to server, all in a single function callFrom client to server, all in a single function call

All of these also need to include error handling and serialization/deserialization "out of the box".

Solution: Effect + Effect Schema

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

Effect rcp: type safe from client to server

Turns out the solution was hiding in plain sight: @effect/rpc πŸͺ„

It works as described above, with the full power of effect for error handling and @effect/schema for serialize/deserialize requests, all provided out of the box:

  1. Define TaggedRequest using @effect/schema (error, success, parameters)
  2. Define shared Router implementation (server requests)
  3. Execute the request on the server (single api endpoint)
  4. Define http request parameters on the client
  5. Execute request just like a normal Effect

This is all type safe end-to-end: the types are shared between client and server πŸ”₯

Use @EffectTS_ rpc on the client, as simple as a function call πŸͺ„ πŸ‘‰ Export HttpResolver that performs a request to the rpc endpoint πŸ‘‰ Make a request with a single function call πŸ‘‰ Both request and return type is fully typed (success and error)

Aleksander Rendtslev
Aleksander Rendtslev

I'd love to see the client side example of this if you have one πŸ™


Type safety everywhere

My journey to type safety continues:

  • The server is as type safe as possible with effect
  • Improvements on client/server communication using @effect/rpc
  • Powerful and type safe state management on the client using xstate

You may have heard the saying: "If it compiles it works" πŸ‘€

This is the final destination. When you reach that ideal one of 2 things can happen:

  • The app works and there are no runtime issues πŸ†
  • The app doesn't compile, which means the problem is "technically" no solvable. This is usually a sign of some missing detail in the requirements or the domain design, that you can solve before releasing in production πŸ†

Speaking of type safety, I am working on something new that may probably interest you (since you are reading this) πŸ‘‡

I am about to announce it soon. Rest assured that you will get to know about it before everyone else as part of this newsletter πŸ‘€

See you next πŸ‘‹

Start here.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.