Skip to content

jalho/poc-http-json-crud-postgres

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

94 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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_one in ./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_one in ./nodejs-equivalent/main.mts:

    async function post_one(inbound: libhttp.IncomingMessage, outbound: libhttp.ServerResponse) {
      // ...
    }

Actor pattern

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.

Diagram of the actors

  • 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::Actor defined 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).

Usage

cargo run

Entry point is at ./src/main.rs.

Cheatsheet

  • 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 books in 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

About

A proof-of-concept implementation for some common ideas: HTTP JSON CRUD API on PostgreSQL using Actor Pattern in tokio ecosystem.

Resources

Stars

Watchers

Forks

Contributors