Rust Webserver with Plugin Architecture


I started this project mainly because I wanted to get deeper into Rust than a few toy programs — and a webserver felt like a good excuse to deal with real concurrency, real I/O, and a design that actually matters. This server is deliberately minimal at its core: it just routes requests and manages plugins. All the actual request-handling logic lives in separate plugin processes that talk to the core over a protocol I designed myself.

Why plugins as separate processes

Architecture

src/
├── main.rs                          # Server entry point
├── lib.rs                           # Library exports
├── webserver/                       # HTTP server implementation
│   ├── webserver.rs                 # Webserver trait
│   └── http_1_server.rs             # HTTP/1.1 implementation
├── plugin/                          # Plugin management
│   ├── plugin_manager.rs            # Plugin lifecycle and routing
│   ├── plugin_entry.rs              # Plugin configuration
│   ├── plugin_config.rs             # Config parsing
│   └── running_plugin.rs            # Active plugin instance
├── plugin_communication/            # Plugin communication layer
│   ├── plugin_communicator.rs       # Communication interface
│   ├── package_handler.rs           # Binary protocol handler
│   ├── models.rs                    # Data structures
│   ├── protocols/                   # Protocol implementations
│   └── app_starter/                 # Process management
└── io/                               # File system abstraction
    ├── data_storage.rs              # Storage interface
    └── in_memory_storage.rs         # Test implementation

Each plugin ships a pluginConfig.json describing its name, startup command, protocol, timeouts, and which HTTP methods/hosts/paths it handles. When several plugins could match the same request, the router falls back to a specificity algorithm — an exact host beats a wildcard subdomain beats *, and the same logic applies to paths, so /api/users wins over /api/* wins over /*.

The protocol itself is a length-prefixed, binary-framed JSON exchange over stdin/stdout: a 4-byte length prefix followed by the JSON payload, with message types for handshakes, requests, responses, and error notifications. Two example plugins — one serving static files, one handling email subscriptions — exist to prove the protocol actually works end to end.

What went well, what didn't

Designing that protocol was the most interesting part of the whole project. I had to think through framing, handshakes, error handling, and how specific a plugin's routing rules are allowed to be — all before writing a single line of the actual server logic. Getting the specificity rules right took a few iterations, but the result is a routing system I trust.

The hardest part was honestly just Rust itself at that stage: getting the ownership model to cooperate with a design built around long-lived processes and inter-process communication meant a lot of fighting the borrow checker before things clicked. In hindsight, that fight is exactly why I stuck with Rust for this — once it compiles, entire categories of bugs just aren't there anymore.