This project is not arranged in the usual fashion you might manage a software project. There are several branches, some built on top of others, but they are not meant to be viewed in terms of their history per se. It may be interesting to look at them as total diffs via a "compare" view, but the lineage of each branch isn't that important.
Each of the branches named here represents a different approach, tech-wise, to solve the problem keeping server and client applications aligned via type-safety enforced by rust and typescript.
In order of implementation, we've got:
- wasm-bindgen branch - uses wasm-bindgen to export an HTTP client package. Alignment is maintained by using rust itself to write the HTTP client, mostly.
- master branch - aka "wasm removal" dumps wasm-bindgen, but uses the
typescript-definitions
crate to export types used in a hand-written Typescript HTTP client package. This is the version with the most manual upkeep required.- graphql branch - uses
juniper
for the server, andapollo
on the client. Both server and client use codegen based on a common schema to enforce alignment.- grpc branch - a similar approach to the graphql branch. Codegen is used on a common
.proto
file to produce code for atonic
server andgrpc-web
client.The work in this branch was carried forward from wasm-bindgen branch.
In this iteration, the broad strokes are much the same, however we shed the adoption of
wasm-bindgen
, instead using a small binary crate to dump out typescript definitions in a mostly handwritten HTTP client.The sections of this document to follow have been updated to account for the changes, but many of the items that have been removed/simplified will be shown with a strikethrough treatment, or pulled out and noted specifically.
When building http server and client apis it can sometimes be a challenge to keep request and response payload shapes aligned.
This project aims to explore options to build both client and server with a shared collection of types. It is my hope that we'll be able to publish client libraries to match servers in lock-step by having a mutual dependency on a rust crate which contains all the "shapes" that can be seen from each side.
The high-level goal here is to:
- build a json-based web api server.
- build a client to match.
- import and use the client from a react app.
- if possible, export Typescript info with the client.
- static analysis (the rust type system) to enforce all projects are aligned at any given time.
Part of the idea here is that adding or removing endpoints in a web api happen more infrequently (in my experience) than changes to the shapes of the payloads.
With this in mind, having to write client calls manually isn't that big of a deal. Having some type checking on both ends for the shapes of the payloads is of higher value, and potentially easier to achieve with less "magic" involved.
There's some complexity in how this all fits together so here's the gist.
In the top level of the repo, there are 4 distinct sections:
e2e-core
(rust): common types shared between client and servere2e-server
(rust): anactix-web
api servere2e-client
(rust, js, Typescript):A higher-level js http client, written inTypescript wrapping the wasm module fromNow, a handwritten HTTP client in Typescript, bundling typescript defs generated by a small binary cargo project.e2e-client-wasm
.web-frontend
(js): this is acreate-react-app
project with some tweaks toadd wasm support. This project depends onboth thee2e-client-wasm
ande2e-client
.
The notable omission here is the
e2e-client-wasm
package, which means we no longer need the nightly rust toolchain for anything and have one fewer package to juggle.It also means we don't have any weird peerDependency relationship between npm packages, no longer need rewire to add custom loaders to our webpack project AND don't have any weird async module imports to deal with.
This section describes the various tools you'd need to be able to build and run this project.
- https://rustup.rs/ (install this to get the latest stable toolchain; minimum version 1.39)
- https://nodejs.org/en/ (download, unpack, and add the bin dir to your PATH, I used 10.16.3 LTS)
https://rustwasm.github.io/wasm-pack/installer/ (just be sure to install this after installing rust)We don't need it anymore!
Once you've got all this stuff installed, you should be able to run the following:
$ cargo build -p e2e-server
$ cd e2e-client
$ npm i
$ npm run build
$ cd ../web-frontend
$ npm i
Ordering is important here in so much as web-frontend
won't be able to install
e2e-client
unless it has already been built.
At this point you should be able to run the project.
In one shell, from the project root directory, run
$ cargo run -p e2e-server
and in another, from the web-frontend
directory, run
$ npm run start
This section describes what was done to put all the pieces together, more or less discussing just the happy path used to bootstrap the project from the ground up.
The e2e-core
and e2e-server
crates are not really that notable. They are
written plainly as any other conventional rust crates are, and so we'll move
directly on to the other sections of the project.
In the wasm-bindgen branch, this package was largely just a wrapper around our wasm module (which was provided by another package for a long list of annoying reasons).
In this version, we've got a small cargo project in there which we call from a
script entry point defined in the package.json
. If you npm run dump
it will
produce a _structs.ts
in the root of the package. This uses the same underlying
typescript generation code we got with the wasm-bingen
output previously, but
it means we need to maintain this list of types to dump (in the main.rs
).
The dump
is automatically run as a part of npm run build
.
Other than the above, this is a plain Typescript project that uses fetch
to
make requests to the backend. Nothing weird or annoying about it.
In the wasm-bindgen branch this project was bootstrapped with Create React App, and then we had to customize it with something called rewire to inject new loaders into the webpack config.
Since this version doesn't rely on wasm, we don't need to do any of that. This time, we only use a plain CRA typescript setup.