Kärntner Linuxtåg 2026 · Villach

Aosabi:GraphQL
everywhere

Ein GraphQL-Server vom Mikrocontroller bis zur Edge.

Ganz kurz zu mir

Florian H. Kleber · Netsnek

Florian H. Kleber

Netsnek ist Co-Sponsor des Kärntner Linuxtågs und hilft mit, die Linuxtage nach Kärnten zu bringen.

Beim Linux-Stammtisch bin ich regelmäßig dabei.

linkedin.com/in/kleberbaumLinkedIn
Aosabi: GraphQL everywhere

Warum GraphQL

Eine API, die sich selbst beschreibt.

Ein Endpoint mit Introspection

Alle Anfragen gehen an /graphql. Das Schema beschreibt sich selbst, Werkzeuge wie GraphiQL lesen es direkt.

Zentrale Validierung von Anfragen

Jede Anfrage wird gegen das Schema geprüft, bevor eine Zeile App-Code läuft.

Objektorientierte API

Die Grundzüge der API ergeben sich aus der Funktionsweise von GraphQL. Als Developer weiß man schon im Vorhinein zirka, was einen erwartet.

Anfrage

{
  song(id: "1") {
    title
    artist { name }
  }
}

Antwort, in derselben Form

{
  "data": {
    "song": {
      "title": "Rock Me Amadeus",
      "artist": { "name": "Falco" }
    }
  }
}
Aosabi: GraphQL everywhere

Warum GraphQL

Eine Anfrage, über jede Leitung.

Keine Roundtrips

REST braucht für Song, Interpret und dessen weitere Songs drei Anfragen. GraphQL holt alles in einer.

REST Client Server GET /songs/1 GET /artists/7 GET /artists/7/songs GraphQL Client Server { song(id: "1") { artist { songs { title } } } }

Funktioniert auch ohne HTTP

GraphQL ist eine Abfragesprache, kein HTTP-Protokoll. Die Leitung darunter ist austauschbar.

HTTPWebSocketSerialUSBBluetoothLoRa

Aosabi spricht GraphQL auch über Serial, USB, Bluetooth und LoRa, mit GraphTP als schlankem Rahmen für jede dieser Leitungen.

Aosabi: GraphQL everywhere

Warum nicht GraphQL

Fünf Gründe, bei REST zu bleiben.

Mehr Moving Parts als REST

Parser, Validator, Executor, Schema und Resolver statt einer Route pro Ressource.

Unperformant und ressourcen­intensiv

Jede Anfrage wird geparst, als Baum aufgebaut und mehrfach durchlaufen.

Schema-Files

Schema und Code liegen getrennt und laufen mit der Zeit auseinander.

Implementierung je Sprache anders

Jede Sprache hat ihren eigenen Server, mit eigenem Verhalten in den Details.

Security

Viel JavaScript, Python und Co. mit langen Ketten an Dependencies.

Vier davon lassen sich beheben.

Aosabi: GraphQL everywhere

Was sich ändern lässt

4 von 5 lassen sich fixen.

Mehr Moving Parts als REST

BleibtGraphQL hat mehr Teile als REST, dafür eine klare Struktur.

Unperformant und ressourcenintensiv

Single PassValidierung in einem Durchgang, in Rust.

Schema-Files

Schema aus OOPDas Schema entsteht aus Klassen und Typen im Code.

Implementierung je Sprache anders

Rust-Core, in JS, Python usw.Ein Kern, der in jeder Sprache gleich antwortet.

Security: JavaScript, Python und Dependencies

Security: Software FactoryAgenten finden und beheben, Menschen entscheiden.

Aosabi: GraphQL everywhere

AgenticBackend Framework

Aosabi: GraphQL everywhere

Speed durch Single Pass und Rust

Ein Durchgang statt Baum und 27 Regeln.

AST-Validierung, wie in graphql-js Endpoint/graphql ParserText zu Baum AST Validator27 Regeln, Knoten für Knoten Executorüber den Baum CodeResolver Single Pass in Aosabi, inspiriert von SOAP Endpoint/graphql Single Passein Durchgang über die Tokens, gegen eine Feldtabelle aus dem Schema Executorgleiche Engine CodeResolver
Feldtabelle aus dem Schema

Das Schema wird beim Build zu einer Tabelle. Jedes Token wird direkt gegen sie geprüft.

Gleiche Fehlermeldungen

Lehnt der Single Pass eine Anfrage ab, prüfen die 27 Regeln nach. Die Antwort bleibt gleich.

Rust

Ohne Garbage Collector und ohne std: derselbe Kern läuft auf Server, Edge und ESP32.

Aosabi: GraphQL everywhere

Generiertes Schema

Das Schema kommt aus dem Code.

Rust · src/lib.rs

use aosabi::prelude::*;

/// A song in the library.
pub struct Song {
    pub title: String,
    pub artist: Artist,
}

pub struct Artist {
    pub name: String,
}

impl Artist {
    pub fn songs(&self) -> Vec<Song> {
        library().by(&self.name)
    }
}

pub struct Query;

impl Query {
    pub fn song(&self, id: ID) -> Option<Song> {
        library().song(id)
    }
    pub fn songs(&self) -> Vec<Song> {
        library().all()
    }
}

pub fn app() -> App { App::new().query(Query) }

TypeScript · src/index.ts

import {App, type ID} from 'aosabi'

/** A song in the library. */
class Song {
  constructor(
    public title: string,
    public artist: Artist
  ) {}
}

class Artist {
  constructor(public name: string) {}

  songs(): Song[] {
    return library.by(this.name)
  }
}

export default new App({
  graphql: {
    Query: {
      song: (id: ID): Song | null =>
        library.song(id),
      songs: (): Song[] => library.all()
    }
  }
})

schema.graphql · generiert aus lib.rs

type Query {
  song(id: ID!): Song
  songs: [Song!]!
}

"""
A song in the library.
"""
type Song {
  title: String!
  artist: Artist!
}

type Artist {
  name: String!
  songs: [Song!]!
}
Aosabi: GraphQL everywhere

Software Factory

Agenten bauen, Menschen entscheiden.

Logs & BefundeLetzte Runde, Produktion PlanAgent BuildAgent ReviewAgent Fix & CheckAgent + 143 Notebooks Human GateMensch entscheidet mainein Commit je Fix Produktion Logaufarbeitung: Jede Runde liest die Logs der letzten Runde und die Logs aus dem Produktivbetrieb
Logaufarbeitung

Jede Runde liest die Logs der letzten Runde und die Logs aus dem Produktivbetrieb, statt von vorne zu beginnen.

Human Gates

Entscheidungen mit Tragweite trifft ein Mensch. Agenten bereiten sie vor.

Security

Findings werden behoben, nie nachgebaut. Jeder Fix ist ein eigener, prüfbarer Commit.

Aosabi: GraphQL everywhere

aoviewer

Die Song-App, wie aoviewer sie zeichnet.

#/whole/Query · der ganze Graph

O Query root type song Song artist Artist songs [Song!]! O Song id ID! title String! year Int! artist Artist! O Artist id ID! name String! songs [Song!]!

#/op/query/song · der Weg der Operation song

O Query root type song(1) +2 fields O Song id ID! title String! year Int! artist Artist! O Artist songs +2 fields
Aus dem Build

Das Schema kommt aus dem Rust-Code, aoviewer zeichnet es als SVG. Genau diese App läuft in den Benchmarks.

Der Kreis

Song.artist führt zu Artist und Artist.songs zurück zu Song. Diesen Weg nimmt die Query von vorhin.

Jede Ansicht eine Route

Der ganze Graph, ein einzelner Typ oder der Weg einer Operation, jede Ansicht hat ihre eigene Adresse.

Aosabi: GraphQL everywhere

Benchmark

Zeit und Größe, gegen REST und graphql-js.

RESTRust, hyper mit TLS und HTTP/2Aosabi in Rustauf Linux, mit TLS und HTTP/2graphql-jsauf Bungraphql-jsauf Node.jsZeit für Song, Interpret und dessen Songsµs, Median aus fünf Runden, weniger ist besser23,4 µs opt-level z18,8 µs opt-level 356,5 µs opt-level z26,7 µs opt-level 3668 µs ohne JIT75,0 µs mit JIT634 µs ohne JIT140 µs mit JITGröße der Binaryausführbare Datei und was sie lädt, weniger ist besser820 KB hyper 815 KB + App 5 KB1,2 MB hyper 1,2 MB + App 10 KB1,9 MB Aosabi 1,9 MB + App 15 KB3,2 MB Aosabi 3,2 MB + App 25 KB93,8 MB Bun 93,8 MB + App 1 KBdieselbe Datei119,0 MB Node.js 119,0 MB + App 3 KBdieselbe DateiAlles in Rust zweimal gebaut, beide mit std aus dem Quellcode und lld: opt-level z ist der Standard und auf Größe optimiert, opt-level 3 auf Tempo.graphql-js je zweimal gestartet: mit JIT wie üblich und ohne JIT nur im Interpreter, Bun mit BUN_JSC_useJIT=0, Node.js mit --jitless.Neben der App steht alles andere: bei Aosabi und hyper auch std, TLS und HTTP/2, bei Bun und Node.js die Laufzeit samt graphql-js.
Song, Interpret, Songs

Derselbe Generator fragt alle Server ab, eine Anfrage nach der anderen, mit den Daten von vorhin. REST braucht drei Anfragen, GraphQL eine.

Ohne JIT

Ohne JIT läuft JavaScript nur im Interpreter. graphql-js wird dann auf Bun fast neunmal und auf Node.js viereinhalbmal langsamer. Aosabi braucht keinen JIT.

Die App selbst

Gezählt wird, was den Server startet. Die App selbst hat überall nur 1 bis 25 KB, der Rest sind Laufzeit und Bibliotheken.

Aosabi: GraphQL everywhere

Puhh,wer kann sich heutzutage noch so viel Speicher leisten?

Aosabi: GraphQL everywhere

Benchmark

ESP32-C3: drei Roundtrips gegen einen.

RESThyper mit HTTP/2 und TLS 1.3Aosabi in Rust1 Anfrage über GraphTPZeit für Song, Interpret und dessen Songsms, Leitung und Rechenzeit, weniger ist besser72,6 ms 3 Roundtrips, 827 Byte32,4 ms 1 Roundtrip, 300 ByteGröße des ImagesApp-Image im Flash in KiB und MiB, weniger ist besser0,82 MiB Firmware 0,81 MiB + App 5 KiB0,99 MiB Aosabi 0,98 MiB + App 10 KiB, unter 1 MiBESP32-C3 in QEMU simuliert, Serial Line auf UART0 mit 115.200 Baud, 8N1, REST gemessen über HTTP/1.1 ohne TLS.Rechenzeit als Untergrenze mit einem Takt je Befehl bei 160 MHz: Aosabi 6,3 ms, REST 0,9 ms, alles andere ist Leitung.Neben der App steht alles andere, bei REST ESP-IDF, hyper und rustls, bei Aosabi auch Homa und GraphTP.
Song, Interpret, Songs

REST fragt dreimal nacheinander, denn erst die erste Antwort nennt den Interpreten. Aosabi fragt einmal.

115.200 Baud

Jedes Byte jeder Anfrage und jeder Antwort braucht 86,8 µs auf der Leitung.

Dieselbe App

Die Song-App von vorhin mit denselben Daten, als eigenes Image je Seite auf einem simulierten ESP32-C3.

Aosabi: GraphQL everywhere

Resultat

Embedded-Demo: ESP32 in Rust.

ESP32-C3Aosabi · Rust · no_std BOOT RST RGB GraphiQL · Web Serial mutation {led(on: true, colour: CYAN)} {"led":{"on":true,"colour":"CYAN"}} {"hello":"Hello from Aosabi"} Laptop, Browser mit Web Serial

Query

{
  hello
  uptime
  heap { free used peak }
  temperature
}

Mutation

mutation {
  led(on: true, colour: CYAN) {
    on
    colour
  }
}
ESP32-C3, C6 und S3GraphTP über SerialImage rund 1 MBgleicher Engine-Kern
Aosabi: GraphQL everywhere

Resultat

Cloudflare Worker: TypeScript mit HTML-Client.

index.html fetch('/graphql', {query: '{ songs { title } }'}) Rock Me AmadeusLive Is LifeFürstenfeld Cloudflare-Edge Worker TypeScript-Appsrc/index.ts, Schema aus den Typen core.wasmAosabi-Engine in Rust, als WebAssembly
TypeScript-App

Klassen und Typen in index.ts, das Schema entsteht daraus.

Rust als WebAssembly

Die Engine läuft als core.wasm direkt im Worker.

HTML-Client

Eine statische Seite fragt /graphql direkt, ohne Framework.

Aosabi: GraphQL everywhere

Benchmark

Cloudflare Worker: Kaltstart und Größe.

AosabiTypeScript, Engine als core.wasmPylonTypeScript mit graphql-jsKaltstart bis zur ersten Antwortms, Median aus 30 Starts, weniger ist besser45,8 ms49,8 msUpload zu CloudflareSkript und WebAssembly, weniger ist besser2,4 MB gzip 897 KB1,3 MB gzip 225 KBKaltstart heißt Skript laden plus erste Anfrage: Aosabi 26,6 + 19,2 ms, Pylon 41,2 + 8,6 ms.Gemessen in workerd, der Laufzeit von Cloudflare Workers.

26

Dependencies weniger im ausgelieferten Worker

Pylon bündelt 29 fremde Pakete in den Worker, Aosabi 3: @cloudflare/unenv-preset und unenv, die Wrangler beiden mitgibt, und once_cell in core.wasm.

Dieselbe App

Die Song-App in TypeScript, einmal mit Pylon und graphql-js, einmal mit Aosabi. Beide antworten byte-gleich.

Warm

Nach dem Kaltstart antworten beide in unter einer Millisekunde, Aosabi in 0,66 ms, Pylon in 0,59 ms.

Upload

Aosabi bringt die Engine als core.wasm mit. Sie macht 2,2 der 2,4 MB aus.

Aosabi: GraphQL everywhere

Danke!

Florian H. Kleberlinkedin.com/in/kleberbaum
Netsnek

Aosabigithub.com/netsnek/aosabi