xd0WMMXkdddddddxKOOkOO0000kdXXXKxxkkdKKKXOOkklcccc:cKkx:;kdc::;l:loo:c:,kOdd;ckccd::o:::od;::kxOOOoxXl,:cdocddkdxxlox:cldxOkx:lodoooooooooooool:,,,,;:cllllllllllllloxd
KNNKOxddddddk0NMXkOkOO00O0KXKOkdxxxoO0KKOkkxlcccc:co0xo,lxd::::o::okc::lOkdl:oo;cc;oo:::xd::lkxOOOkKx:;,oddcodkdddocxlccddkkkclcoooooooooooooooolc:;,ii,,,;;:::ccllllld
Oxddddddk0XWNKOdxOxOO0KKXK0kxxxdddlOOK0kkkxc::cc:c:0xd,;xdd,:;cd:::dl:cOOxdc:d:;:;odc::ldo::kodoooxd:;;;cool:ddoddd;do:ccxdkxdcoooooooooooooooooooollc::;,,iiiiiii,,;;:
ddddk0XWXKOxdddxkkO0KK0OkkxxdddxxlkOXOkkkdc::cc:::lkx:,:kdl,;,co:;;;lloxOxo:ol;:;cdd,:codl:odoOOOOkdx:lccoddcodoodd;cd:::odxxxoooooooooooooooooooolllllllllllcc::;;;,,,
k0XNX0kdddxxxkkO000OOkkkxxxkOOkdck0Xkkxxo:::cc:;::dxo,;cdo:,;;odl;;;codkkdolc:::cdxd:clxxclOdOKXKkxkkkdd:ooddcddodd::d:::cdddooloooooooooooooooool;clllllllllllllllllll
KOxdddddxxddddddxxxxxddO0K0OkdocO0Kkxxxl:;;:::;:::xd;;;odd;;:lddl:;:ldddOxol::ccxxxklldxdl000NNX0kk0X0kdocddxccdddd::o:cccoxolloooooooooooooooool;,:llllllcclllllllllll
dddddddddddddodxkkkk0KXKOkxdolxO0Kkxdxc;;;::::;:;cxo;;;odd,:cdddl;:cOkdxxkoccllxkxxOldkkoKN0NNXK0kKNNNKOdlckdd:dxoxccl:clclxdloooooooooooooooooo;,i:lllllll:,:cllllllll
dddddddddollOXxxx0KXKOkxollcl0O0Kkdoxc;;;;:::;:::lxc:::ddd,:ddxxc;:kOOkxkdclooxkkxOOdkOk0N0NNXX0OXXK00Okkdllkdllxdxccl:cllcdxodooooooooooooooooc,,i:llllllll:ii,;:cllll
dddddxOxclOWWkx0NX0kxolccc:oKkOKkxlxl;;;;::::,:::od::::odo:ckdxx::dOOOOOoloodxOOkOXkk0OKNKNNXN0Okxolc,;;i;,,colcddxlll;coocooxooooooooooooooooo,,ii;lllllllll:iiiii,;cl
xk0XW0lo0MMMO0XKOdolool:::dKkkXkklxo,;;;:::::;:::xl:::;cxdclOxxd::dodxxxkxxdxO0OkKNOOKKNXXXXNK0Od,.i,;::::;,:,;.:loool:lddllodoooooooooooooooo:,iii:llllllllll:iiiiii,;
MMW0ldXMMMWXKkdxodxdl::::xKOxNOkoox;,;:;:::c;::::xc:::,oxo:,i;:,.....,lxOOdkkKKkKNK0KKNXKXNNNNNO:xxc,i...i;ckOkdll::clooxdlcxdooooooooooooooodiiiii:lllllllllll;iiii,,,
NklxNMMMMXOxoxxoddo:::::OKOxX0Okckc,,;;::::o,::::x:::;,,;;:;cdxocccoxxldXXkOKKO0NNKXKNXXNNNNNNNOXl...........lK0Okxl:lodkdlcxooooooooooooooooliiiiicllllllllllll;iiii,,
ckNMMMMMOdodxdodo:;:::;OKOkxXOOodd,,,;;:::lo,:::co:;;..:xkxo,c:......,xKKNO0XK0NNXXXNXNNNNNNNNNNx.....ii......oNX0kllxxkkx:lxoooooooooooooolo:iiii,llllllllllllll;ii,,,
NMMMMMWOkodxoodc;:::;;kK0koK00klx;,;;;:::cdo,:::cc;,,lkk00:..c:........oNN0XX0NNNKXNXNNNNNNNNNNNci,i.:dXl;.....WNKdoxOkOkx;lxooooooooooooolld,iiii;lllllllllllllll,iii,
MMMMMM0Ooddodo:;:c::;xX0kooK00ddl;;:;;::codo,c::l:;,;kKXX0,..li,:o:i....WNKXKNNNXXXNNNNNNNNNNNNWXKXo::oxl;;cc:;MMKkOkOkOOo,cxdooooooooolllllliiiiiclllllllllllllll:iiii
MMMMMNkodoodc;;cl:;;oX0kdodK0Ood;;::;,cccxxo,cccc:::icXNN0,,,c.lOW0l;od;MNXKNNNNKNNNNNNNNNNNNNNNNokxocc:::lkKoXMW0KOOxOO0c,;xdlllllldolllllo:iiiiilllllllllllllllll,iii
MMMMWxooloo:;:ld:;;cX0xdodd00kdo;:::;;:clkxo,:cc::::oclXWN;dkcc::c:;lx0cXXXXNNNKNNNNNNNNNNNNNNNNXNK000000XX0dXNNN00OOxOOO:,,oxolllllollllllo,iiii,lllllllllllllllll;iii
MMMWdollol;;codc;:;O0xxxolc00kxc;:::;,;cxkxd;;ccc:c:xOKXWNNXO0xlodxxOK0XNNXNNNXNNNNNNNWNNNNNNNNNNNNXXXXXXXXXXXWNN0kOkOO0x:,,:dxollllllllllloiiiii;lllllllllllllllll:iii
MMWolclo:;:lodo;;:o0xxxd::Ok0Ox:::::;,;cOxxx:;clccc:kOXNNXNW0x0XXXXXXNNNNNNNNNNNNNNNNKONNNNNNNNNNNNNNXXXXXXXXXXNNOOOk00Od;i,,:dxdllllllllllliiiii;lll:lllllllllllllcii;
XOlc:ol;;cooxo:;;:Oxxxo:;lO:OOxc::::;,;:Kkkkcc:lccllOOXNNNXXXXXXXXXXXXXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNXKOx000d:..,,,;ldxdolllllllciiiii;llc,lllllllllllllci,c
dc::ol,;ldoxxc;;;okxko:::od:cOxl:::;;,;;KOkklcclccllOOKNNNNWNXXXXXXXXXNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNXkk0Kkol;.i,,,,:oddolc;,,iiiiiii,llcicllllllllllllci;l

Hit the GRTTy!

GRTTy Stack: gRPC Rust React Typescript

NOTE: You can find the code for a working full stack example repository here https://github.com/chuu-p/GRTTy

Why did I choose this stack?

gRPC offers strong typing, fast performance, and native streaming. Compared to OpenAPI, it’s more efficient for internal service communication but slightly more complex to work with in frontend environments.

Rust is my favourite programming laguage. Diesel is my favourite database crate, beacause you write sql queries as rust daisychain functions, instead of raw sql, which I like a lot. I chose Sqlite as the database for this example, other database systems (PostgreSQL, SQLite, or MySQL) can be used, too.

Typescript is less horrible then Javascript, React is the most popular frontend framework, and the one I know best. I dont like separate CSS files, insted putting the styling directly inside the react components. In the past I used MUI, for this project I chose Tailwind for a more custom look.

API-Specification

A frontend/backend contract via gRPC is a shared .proto protobuf file that defines all the requests, responses, and services (API functions) the frontend and backend use to talk to each other.

Both sides use this file to generate code, so they always agree on the exact data types and endpoints — ensuring they don’t accidentally break the communication.

It’s like a single source of truth for the API.

Create the root project directory, cd into it and init git repo.

$ mkdir example
$ cd example
$ git init

Now we create the gRPC .proto api contract and set some environment variables. Both the frontend and backend will use both files.

example-spec/example.proto

syntax = "proto3";

package example;

service HealthCheckService {
  rpc CheckHealth(HealthCheckRequest) returns (HealthCheckReply) {}
}
message HealthCheckRequest {}
message HealthCheckReply {}

example-spec/.env

VITE_GRPC_SERVER_ADDRESS=127.0.0.1
VITE_GRPC_SERVER_PORT=50051
VITE_FRONTEND_ADDRESS=127.0.0.1
VITE_FRONTEND_PORT=5173
DATABASE_URL=example.db

Frontend

I use pnpm as a package manager, but u can use npm, pnpm, yarn, bun, or whatever you like.

On naming:

# 1. create the frontend project
$ pnpm create vite example --template react-ts
$ cd example

# 2. add tailwind
$ pnpm install tailwindcss @tailwindcss/vite
$ pnpm approve-builds # press: a -> enter

example/vite.config.ts

import path from "path";
import {defineConfig} from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), tailwindcss()],
  envDir: path.resolve(__dirname, "../example-spec"),
});

add protobuf codegen and generate the typescript gRPC server and gRPC client code from the gRPC spec:

$ cd example

#  protobuf codegen & runtime
$ pnpm install -g protoc-gen-ts
$ pnpm add @protobuf-ts/plugin @protobuf-ts/grpcweb-transport @protobuf-ts/runtime @protobuf-ts/runtime-rpc google-protobuf @grpc/grpc-js

# generate the typescript gRPC server and gRPC client code
$ mkdir src/protobuf-ts-gen
$ npx protoc -I=../example-spec/ example.proto --ts_out=./src/protobuf-ts-gen

you need to run the npx protoc command every time you change the example.proto spec.

now we add the "health check backend" functionality to frontend.

delete example/src/App.css

example/src/App.tsx

import {useState} from "react";
import {GrpcWebFetchTransport} from "@protobuf-ts/grpcweb-transport";
import {HealthCheckServiceClient} from "./protobuf-ts-gen/example.client";
import {
  HealthCheckRequest,
  HealthCheckReply,
} from "./protobuf-ts-gen/example";

const BACKEND_URL = `http://${import.meta.env.VITE_GRPC_SERVER_ADDRESS}:${import.meta.env.VITE_GRPC_SERVER_PORT}`;

const transport = new GrpcWebFetchTransport({
  baseUrl: BACKEND_URL,
});
const echoClient = new HealthCheckServiceClient(transport);

export default function HealthCheck() {
  const [outputValue, setOutValue] = useState(
    "Click 'Check Health!' to trigger a health check",
  );

  const handleSubmit = async () => {
    console.log(`Sending Request to ${BACKEND_URL}`);
    echoClient
      .checkHealth(HealthCheckRequest.create({}))
      .then((res) => setOutValue(`Success! ${res}`))
      .catch((e) => setOutValue(`Error! ${e}`));
  };

  return (
    <>
      <div className="max-w-2xl p-8 text-center">
        <h1 className="text-3xl font-bold underline">Hello world!</h1>
        <button className="border p-1 rounded shadow" onClick={handleSubmit}>
          Check Health!
        </button>
        <p>{outputValue}</p>
      </div>
    </>
  );
}
$ cd example
$ pnpm run dev
# Now press "Check Health!"
# -> Error! RpcError: NetworkError when attempting to fetch resource. Code: INTERNAL Method: example.HealthCheckService/CheckHealth

There is no backend / gRPC server yet!

Backend

We will create a rust project for the the gRPC Server.

$ cd ..
$ cargo new example-api
$ cd example-api
$ cargo add --build tonic-build

example-api/cargo.toml

[package]
name = "example-api"
version = "0.1.0"
edition = "2021"

[dependencies]
prost = "0.13"
tower-layer = "0.3.3"
http = "1"
hyper = "1"
hyper-util = "0.1.4"
bytes = "1"
tracing = "0.1.16"
tokio = { version = "1.43.0", features = ["full"] }
tokio-stream = { version = "0.1.17", features = ["full"] }
tonic = "0.12.3"
tonic-web = "0.12.3"
tower = "0.5.2"
tower-http = { version = "0.6.2", features = [
  "cors",
  "compression-full",
  "fs",
] }
dotenvy = "0.15.7"
prost-types = "0.13.5"
log = "0.4.26"

[build-dependencies]
tonic-build = "*"

[dev-dependencies]
tokio-stream = { version = "0.1.17", features = ["net"] }

example-api/.gitignore

/target
*.db

example-api/build.rs

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("../example-spec/example.proto")?;
    Ok(())
}

example-api/src/main.rs

use std::env;
use std::path::Path;
use std::time::Duration;
use tokio::net::TcpListener;
use tonic::{transport::Server, Request, Response, Status};
use tower_http::cors::{Any, CorsLayer};

use openfg::health_check_service_server::{HealthCheckService, HealthCheckServiceServer};
use openfg::{HealthCheckReply, HealthCheckRequest};

pub mod openfg {
    tonic::include_proto!("example");
}

#[derive(Default)]
pub struct HealthCheck {}

#[tonic::async_trait]
impl HealthCheckService for HealthCheck {
    async fn check_health(
        &self,
        request: Request<HealthCheckRequest>,
    ) -> Result<Response<HealthCheckReply>, Status> {
        println!("Got a request from {:?}", request.remote_addr());
        let reply = openfg::HealthCheckReply {};
        Ok(Response::new(reply))
    }
}

pub async fn serve(listener: TcpListener) -> Result<(), tonic::transport::Error> {
    Server::builder()
        .accept_http1(true)
        .layer(
            CorsLayer::new()
                .allow_origin(Any)
                .allow_headers(Any)
                .allow_methods(Any)
                .max_age(Duration::from_secs(60) * 30),
        )
        .layer(tonic_web::GrpcWebLayer::new())
        .add_service(HealthCheckServiceServer::new(HealthCheck::default()))
        .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
        .await
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    dotenvy::from_path(Path::new("../example-spec/.env"))?;
    let server_addr = env::var("VITE_GRPC_SERVER_ADDRESS")?;
    let server_port = env::var("VITE_GRPC_SERVER_PORT")?;
    println!("server starting");
    let listener =
        tokio::net::TcpListener::bind(format!("{}:{}", server_addr, server_port)).await?;

    tokio::spawn(async { serve(listener).await });
    println!("server started");

    println!("Waiting now.");
    let mut buffer = String::new();
    std::io::stdin().read_line(&mut buffer)?;

    Ok(())
}

Starting the server:

$ cd example-api
$ cargo run
...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.35s
     Running `target/debug/example-api`
server starting
server started
Waiting now.

We can check the server works by running this command in a separate terminal:

$ grpcurl -plaintext -import-path ./example-spec/ -proto example.proto '127.0.0.1:50051' example.HealthCheckService/CheckHealth
{}

If we check the frontend and press 'Check Health!' -> Success! [object Object]

In the server terminal: Got a request from Some(127.0.0.1:47140)

It Works!

Database

A fullstack ususally includes a database. For this project I chose Sqlite, since this is a minimal example and it is the best choice for the real project I am developing, which inspired the GRTTy Stack.

example-api/cargo.toml

[dependencies]
...
chrono = { version = "0.4.39", features = ["serde"] }
diesel = { version = "2.2.7", features = [
  "chrono",
  "returning_clauses_for_sqlite_3_35",
  "sqlite",
] }
diesel_migrations = { version = "2.2.0", features = ["sqlite"] }
$ diesel setup
$ diesel migration generate create_health_checks

example-api/migrations/.../up.sql

CREATE TABLE
    health_checks (
        id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
        timestamp DATETIME NOT NULL
    );

example-api/migrations/.../down.sql

DROP TABLE health_checks;

Run the migration.

$ diesel migration run

example-api/src/models.rs

use chrono::NaiveDateTime;
use diesel::{prelude::*, sqlite::Sqlite};

use crate::schema::health_checks;

#[derive(Queryable, Selectable, Debug, Clone, Copy)]
#[diesel(table_name = health_checks)]
#[diesel(check_for_backend(Sqlite))]
pub struct HealthCheck {
    pub id: i32,
    pub timestamp: NaiveDateTime
}

#[derive(Insertable)]
#[diesel(table_name = health_checks)]
pub struct NewHealthCheck {
    pub timestamp: NaiveDateTime
}

example-api/src/schema.rs

// @generated automatically by Diesel CLI.

diesel::table! {
    health_checks (id) {
        id -> Integer,
        timestamp -> Timestamp,
    }
}

example-api/src/main.rs

use crate::models::NewHealthCheck;
use chrono::Utc;
use diesel::prelude::*;
use diesel::{Connection, SqliteConnection};
...
pub mod models;
pub mod schema;

pub fn establish_connection() -> SqliteConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    println!("Connecting to {}", database_url);
    SqliteConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
...
#[tonic::async_trait]
impl HealthCheckService for HealthCheck {
    async fn check_health(
        &self,
        request: Request<HealthCheckRequest>,
    ) -> Result<Response<HealthCheckReply>, Status> {
        println!("Got a request from {:?}", request.remote_addr());

        let mut connection = establish_connection();
        let record = NewHealthCheck {
            timestamp: Utc::now().naive_utc(),
        };

        let expected_records = diesel::insert_into(health_checks::table)
            .values(&record)
            .execute(&mut connection)
            .unwrap();

        debug_assert_eq!(1, expected_records);

        let reply = openfg::HealthCheckReply {};
        Ok(Response::new(reply))
    }
}
...

Now we start the server, press the button in the frontend, and check for a new entry in the database.

$ cargo run
...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.72s
     Running `target/debug/example-api`
server starting
server started
Waiting now.
Got a request from Some(127.0.0.1:52352)
Connecting to openfg.db
^C

# check if the entry was written in the db
$ sqlite3 openfg.db "SELECT * from health_checks;"
1|2025-03-02 00:28:04.312533388

Tests

Backend

$ mkdir example-api/tests

example-api/tests/health_check.rs

use diesel::prelude::*;
use example::health_check_service_client::HealthCheckServiceClient;
use example::HealthCheckRequest;
use example_api::schema::health_checks;
use example_api::{establish_connection, serve};
use std::env;
use std::path::Path;

pub mod example {
    tonic::include_proto!("example");
}

#[tokio::test]
async fn health_check_works() -> Result<(), Box<dyn std::error::Error>> {
    // arrange
    dotenvy::from_path(Path::new("../example-spec/.env"))?;

    let server_addr = env::var("VITE_GRPC_SERVER_ADDRESS")?;
    let server_port = env::var("VITE_GRPC_SERVER_PORT")?;

    println!("server starting");
    let listener =
        tokio::net::TcpListener::bind(format!("{}:{}", server_addr, server_port)).await?;
    tokio::spawn(async { serve(listener).await });
    println!("server started");

    // empty database
    let mut connection = establish_connection();

    diesel::delete(health_checks::table).execute(&mut connection)?;
    assert_eq!(
        Ok(0),
        health_checks::table.count().first::<i64>(&mut connection)
    );

    let client_addr = format!("http://{}:{}", server_addr, server_port);
    println!("client starting on: {:?}", client_addr);
    let mut client = HealthCheckServiceClient::connect(client_addr).await?;
    println!("client connected");

    let request = tonic::Request::new(HealthCheckRequest {});

    // act
    let response = client.check_health(request).await?;

    // assert
    println!("RESPONSE={:?}", response);

    // check response status
    let status = response
        .metadata()
        .get("grpc-status")
        .unwrap()
        .to_str()
        .unwrap();
    assert_eq!("0", status);

    // check database entry
    let rows = health_checks::dsl::health_checks
        .count()
        .get_result::<i64>(&mut connection)
        .unwrap();
    assert_eq!(1, rows);

    // let id = health_checks::dsl::health_checks
    //     .select(health_checks::id)
    //     .first(&mut connection);

    Ok(())
}

Frontend

I will get to this soon ^^

TODO unwraps -> nice errror handling!

example/src/App.test.ts

import { expect, test } from 'vitest'

test('adds 1 + 2 to equal 3', () => {
  expect(1 + 2).toBe(3)
})

Sources

Misc

I use just to save and run project-specific commands. This is the justfile:

justfile

set shell := ["fish", "-c"]

fdev:
  cd example && pnpm run dev

fgen:
  cd example && pnpm dlx protoc -I=../example-spec/ example.proto --ts_out=./src/protobuf-ts-gen

bdev:
  cd example-api && cargo watch -x run

btest:
  grpcurl -plaintext -import-path ./example-spec/ -proto example.proto '127.0.0.1:50051' example.HealthCheckService/CheckHealth

ci: test fmt clippy # audit # coverage

audit:
  cd example && pnpm audit
  cd example-api && cargo deny check advisories

test:
  cd example && pnpm test run
  cd example-api && cargo test --all-features

fmt:
  cd example && pnpm prettier --check .
  cd example-api && cargo fmt --all -- --check

clippy:
  cd example && pnpm eslint --max-warnings=0
  cd example-api && cargo clippy -- -D warnings

coverage:
  cd example && pnpm test -- --coverage
  cd example-api && cargo tarpaulin --ignore-tests

unused:
  cd example && pnpm prune
  cd example-api && cargo +nightly udeps --workspace

outdated:
  cd example && pnpm outdated
  cd example-api && cargo outdated --root-deps-only --workspace

migrate:
  cd example && npx protoc -I=../example-spec/ example.proto --ts_out=./src/protobuf-ts-gen
  cd example-api && diesel migration redo --all

fmtwrite:
  cd example && pnpm prettier --write .
  cd example-api && cargo fmt --all




Made with <3 in 2025