Proof-of-concept implementation demonstrating:
-
HTTP JSON API with ergonomics of serde ecosystem and axum
-
CRUD operations on PostgreSQL with ergonomics of diesel
-
Using actor pattern in tokio ecosystem (inspired by Alice Ryhl: Actors with Tokio, RustLab Conference 2022)
I wrote this POC to motivate why someone might want to prefer writing an HTTP JSON CRUD API using Rust and its libraries axum and serde, instead of using TypeScript in Node.js without any libraries. Having to choose between these two alternatives is obviously quite an arbitrary restriction, and I'm sure there exists somewhat equivalent libraries in the TypeScript and/or Node.js world too. Anyway, let's suppose you do have to deal with HTTP JSON CRUD APIs using TypeScript in Node.js without any libraries and you happen to like Rust and you are looking for a better way of doing things. When that's the case, please compare the equivalent implementations in Rust and TypeScript:
-
Rust with axum and serde, etc.: See function
post_onein./src/web/handlers/books_v1.rs:pub async fn post_one( axum::extract::State(mut shared): axum::extract::State<crate::web::Shared>, axum::extract::Path(genre): axum::extract::Path<api::Genre>, axum::Json(book): axum::Json<api::BookUnpopulated>, ) -> axum::http::StatusCode { // ... }
-
TypeScript in Node.js without any libraries: See function
post_onein./nodejs-equivalent/main.mts:async function post_one(inbound: libhttp.IncomingMessage, outbound: libhttp.ServerResponse) { // ... }
Actor pattern is a useful idea when we want to clearly define which component in a concurrent program's architecture owns each specific I/O resource. More generally, some fundamental characteristics of actors are:
-
Each actor is the sole owner of something (a network socket, an opened file, or whatever).
This is particularly useful to keep in mind when designing software that is implemented in Rust because as a language it has strong ownership semantics.
-
Actors communicate with each other.
The the communication primitives may be provided by the platform (operating system) or some other framework (some library).
-
All actors may perform their own independent workloads concurrently.
For example, a web server actor may keep accepting network connections at the same time as a program termination actor keeps listening for a termination signal from the operating system.
-
Actors may create new actors.
For example, a web server actor that accepts network connections may create actors that are responsible for a single established connection.
-
The database actor owns the I/O resources of the connection established to a PostgreSQL instance. In the proof-of-concept implementation, the actor is
db::Actordefined in./src/db/mod.rs. -
Each connection actor owns the I/O resources of a single inbound web request.
In the proof-of-concept implementation, connection actors are the individual functions in module
web::handlers::books_v1, which is defined in./src/web/handlers/books_v1.rs.Connection actors are created by a web server that accepts connections. The web server can also be thought of as an actor, and it's defined in
./src/web/mod.rs. -
The terminator actor owns a global shutdown signal. All other actors are connected to the terminator such that they only perform their jobs until the global shutdown signal is activated.
Some actors are also connected to the terminator such that they can activate the global shutdown signal. Intent is that the whole program's graceful shutdown can be initiated by any of the essential actors in case a non-recoverable error occurs.
Terminator also listens for the standard OS level termination signals (
SIGINT,SIGTERM).
cargo runEntry point is at ./src/main.rs.
-
Starting a containerized PostgreSQL instance (using Podman v4.3.1):
podman run --rm \ --name poc-postgres \ -e POSTGRES_PASSWORD=postgres \ -p 127.0.0.1:5432:5432/tcp \ docker.io/library/postgres:17.6-trixie@sha256:feff5b24fedd610975a1f5e743c51a4b360437f4dc3a11acf740dcd708f413f6
-
Creating a table named
booksin the containerized PostgreSQL instance:podman exec -it poc-postgres psql -U postgres -d postgres -c ' CREATE TABLE books ( id UUID PRIMARY KEY, removed_at_utc TIMESTAMP WITHOUT TIME ZONE NULL, title VARCHAR(256) NOT NULL, genre VARCHAR(256) NOT NULL, page_count INTEGER NOT NULL );'
-
POST a book:
curl http://127.0.0.1:8080/api/books/v1/genre/horror --json '{"title":"Foo Bar!","page_count":123}' -
GET books:
curl http://127.0.0.1:8080/api/books/v1