Should you use Rust for your API?

Cover Image for Should you use Rust for your API?

I've recently been building a web application for the FIICode's annual web development competition. I am building it in Next.js, but I have been wondering if perhaps using Rust for the API would be faster than using JavaScript.

In this article, I'll do some tests and see if it's worth running Rust as a backend.

Simple Hello World

const express = require('express');
const app = express();

app.get('/hello/:name', (req, res) => {
    res.send(`Hello from Express, ${req.params.name}!`);
});

app.listen(3031, () => {
    console.log('Server listening on port 3031');
});
use warp::Filter;

#[tokio::main]
async fn main() {
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello from Warp, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

The code is very simple: making a request to localhost:3000/hello/World will return Hello from Warp, World! and making a request to localhost:3001/hello/World will return Hello from Express, World!

And, unsurprisingly, both of them have a response time of less than 2ms. The reason for this is that even though Rust is much, much faster than JavaScript, they're not performing any complex calculations. Because of this, Rust doesn't have the upper hand on JavaScript.

Prime Numbers

Determining whether a given number is prime can be computationally intensive, especially for large numbers.

const express = require('express');
const app = express();

function isPrime(n) {
    if (n <= 1) return false;
    if (n <= 3) return true;
    if (n % 2 === 0 || n % 3 === 0) return false;

    let i = 5;

    while (i * i <= n) {
        if (n % i === 0 || n % (i + 2) === 0) return false;
        i += 6;
    }
    return true;
}

app.get('/test/:number', (req, res) => {
    res.send(`Express: ${isPrime(req.params.number)}!`);
});

app.listen(3031, () => {
    console.log('Server listening on port 3031');
});
use warp::Filter;
fn is_prime(n: i64) -> bool {
    if n <= 1 {
        return false;
    }

    if n <= 3 {
        return true;
    }

    if n % 2 == 0 || n % 3 == 0 {
        return false;
    }

    let mut i = 5;
    while i * i <= n {
        if n % i == 0 || n % (i + 2) == 0 {
            return false;
        }
        i += 6;
    }
    return true
}

#[tokio::main]
async fn main() {
    let hello = warp::path!("test" / i64)
        .map(|n| format!("Warp: {}", is_prime(n)));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

For these tests, Warp (Rust) averages ~1ms and Express (JavaScript) averages ~4ms.

Fibonacci Sequence

const express = require('express');
const app = express();

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

app.get('/test/:number', (req, res) => {
    res.send(`Express: ${fibonacci(req.params.number)}!`);
});

app.listen(3031, () => {
    console.log('Server listening on port 3031');
});
use warp::Filter;

fn fibonacci(n: i64) -> i64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[tokio::main]
async fn main() {
    let hello = warp::path!("test" / i64)
        .map(|n: i64| format!("Warp: {}", fibonacci(n)));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

This test will be a bit more different, as this Fibonacci sequence will be calculated using recursion, which isn't efficient at all. As the input, the program will calculate the 46th number of the Fibonacci sequence. JavaScript averages at about 31 seconds, whereas Rust takes the crown with 5 seconds.

Brute force Sum

const express = require('express');
const app = express();

function bruteForceCubeSum(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
}

app.get('/test/:number', (req, res) => {
    res.send(`Express: ${bruteForceCubeSum(req.params.number ** 3)}!`);
});

app.listen(3031, () => {
    console.log('Server listening on port 3031');
});
use warp::Filter;

fn brute_force_cube_sum(n: i64) -> i64 {
    let mut sum = 0;
    for i in 1..=n {
        sum += i;
    }
    sum
}

#[tokio::main]
async fn main() {
    let hello = warp::path!("test" / i64)
        .map(|n: i64| format!("Warp: {}", brute_force_cube_sum(n*n*n)));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

This test left me with a few question marks, because even though it does perform a lot of calculations they both have almost the same speed, but return a different number:

Warp: 5903443935200918835

Express: 5903443933549936000

Warp (Rust) seems to be more accurate, but Express (JavaScript) is faster by a few ms on average (~70ms).

Conclusion

Rust is almost always faster, except if the API is a really simple one, then it makes more sense to just use whatever you prefer.