Let's start with a disclaimer: I am a PHP developer.
I tried to write a (simple) RESTful API in Rust a while ago, using hyper+tokio, and my pain points were:
No framework except for mio-based hyper works with stable Rust
Rust is already hard to sell, being a new language, with its very small talent pool to hire. If I said "we need to use nightly" they would just laugh at me.
So it's not an option.
It's extremely hard to have both async and multithreading.
I tried and almost ended up rewriting hyper from scratch.
We have both IO-bound and CPU-bound endpoints in our API, so a simple async service doesn't work very well, but we also have very IO-bound (disk, MongoDB, S3) endpoints where both webserver-level and local async operations would be wonderful.
Dependency injection was impossible to achieve
This is probably my fault. Probably I still don't understand "the way of rust" yet, so I was thinking like in PHP.
In all our endpoint we need to inject several objects into our controllers/actions. We have dozens of actions with different dependencies. Most of the examples for rust frameworks are with just 2-3 endpoints, defined as closures in the router. That would be an impossible mess to manage for us. We need separate classes (or structs) for the actions, or at least for the resources (in REST terms).
Some of the dependencies are services (a Mongo collection, the S3 client, etc.). Some are "one shot" (an array of strings allowed for a query, an s3 bucket based on the connected user, or the day of the month, etc.). I didn't find a documented way to do it.
Logging is hard
Compare Monolog in PHP:
$log = new Logger('name');
$log->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));
$log->warning('Foo');
with slog:
let log_path = "your_log_file_path.log";
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(log_path)
.unwrap();
let decorator = slog_term::PlainDecorator::new(file);
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let drain = slog_async::Async::new(drain).build().fuse();
let _log = slog::Logger::root(drain, o!());
Besides that, decorating the logger is quite complex. We have this class:
class ContextLogger implements LoggerInterface {
public function __construct(string $context, LoggerInterface $logger, array $extra = []) {
$this->context = $context;
$this->logger = $logger;
$this->extra = $extra;
}
public function log(string $level, string $message, array $data = []) {
$data['context'][] = $this->context;
$data += $this->extra;
return $this->logger->log($level, $message, $data);
}
}
which allows to do this:
// $logger is injected
$childLogger = new ContextLogger('Process child', $logger);
$this->spawnChildren(new ChildProcess($childLogger), 10);
$logger->info('Children started');
This will log all parent messages "plainly", and all children's message with an additional contextual array of... contexts.
It's hard to work with web data
Iron does a decent job to ease this, but the documentation is quite lacking. Inline docs can be useful to lookup what you already know, but without a good tutorial and detailed explanations on how to use the components, it's nearly useless.
There is no example on how to use cookies, for example, or on how to write middlewares, or a semi-automatic way to decode data coming from the client based on the Content-type (automatic JSON, multipart or uploaded files decoding), or an easy way to add attributes to a request in a middleware.
Take a look at the PSR-7 to see what I mean, or to the Slim framework's Request object and its getParsedBody() method.
The web is stringly-types, so there should be a way to "accept anything and check later". I know this means dynamically-typed structures, but now serde is 1.x and having something like serde-json's Value "integrated" in the framework would make a difference, IMHO.
My wishes
I didn't manage to go much further, to be honest, but another nice thing I would have liked was a way to have multiple protocols managed by the same binary. For example HTTP on a port and gRPC on another. Maybe it's possible, especially with tokio, but again, the documentation is quite lacking.
The idea I got is that the quest for purity and performance is probably getting in the way. Sometimes it's good to give up 20% or even 50% of the performance, or just allocate a struct instead of passing references around to give the developer an easy interface to work with.
We are now working with servers giving us up to 200 req/sec per thread. We don't need to go to 100,000 req/sec. We would be extremely happy with 5,000 or even 2,000, but if it takes 3 weeks to develop the same thing we do in 2 days in PHP, it's never gonna be used. We'll just pay for bigger servers. It's cheaper.
To sum up, I would like to thank everybody working on these projects. I know it's not easy, because I tried.
Rust is growing extremely fast, and I have no doubt it will be great, but it's still too early for web-based sites and APIs, IMHO.