The dream about avoiding API-Endpoints and API-Calls (is it worth it?)
Tags: rust, webdev, api, javascript
Published: 2022-11-06 02:04:16

Hey guys,
there's this idea stuck with me for a real long time.

Imagine you build a small webapp and you don't care about how your API-Endpoint for database queries would look like. So urls like "endpoint-0", "endpoint-1", etc. aren't a dealbreaker. You could just let a compiler generate the api for you!

Before I get into the details I want to explain, how apis are commonly build:
## Server API-Endpoint
* create a function that handles the database query
* gather the parameters from the request
* serialize the result
* think about a good url (optional: store the url into a definition file -> for easier refactoring)

## Client-Request
* create a second function that makes the api call
* pass all needed information in a correct way to the endpoint
* deserialize the response
* then return it

## Pseudocode
What if you'd just write one function and the serialization and api-call part would happen automated?
You'd just write:
```js
// Server side code
// generates intern api-endpoint
// function body will be replaced with an api-call to that endpoint
async function getPost(id) {
  return db.first("SELECT * FROM Post WHERE id=?", id);
}

// ...
// Client side code (can import the auto generated getPost(...) function)
console.log(await getPost(1));
```
It looks very clean and easy to understand.

## Downsides
But: you also don't know exactly what happens under the hood, it's harder to think about authentication, authorization, etc.
And that's actually why I would call this a bad practice... Nevertheless I like this idea very much :D

## Prototype:
Because I don't have the knowledge to make a plugin for a bundler like vite oder webpack, I thought I'd give it a try with Rust. 
Here is a working protype:
```rust
#[macro_use] extern crate rocket;
use rocket::{Build, launch, Rocket};
use serde::{Serialize, Deserialize};
use serde_json;

#[derive(Serialize, Deserialize, Debug)]
struct Data {
    pub foo: String
}

// proc-macro would be much nicer! (but I'm too lazy :D)
ssf!{
    "/api/foo", // api-endpoint for foo(...)
    foo_route, // reference to the endpoint so we can mount it later on
    // the following function body will be replaced with an http-call
    async fn foo(text: String) -> Data {
        Data {
            foo: text
        }
    }
}

#[get("/test")]
async fn test() -> String {
    let data = foo("bar".to_string()).await;  // does a http request to /api/foo with data { text: "bar".to_string() }
    serde_json::to_string(&data).unwrap() // Response is '{ "foo": "bar" }'
}

#[launch]
fn rocket() -> Rocket<Build> {
    rocket::build().mount("/", routes![foo_route, test])
}
```

And here you've got the macro:
```rust
macro_rules! ssf {
    ($path:literal, $apiname:ident, $(vis:vis)? async fn $fn:ident ( $($name:ident : $type:ty),*  ) -> $ret:ty $body:block ) => {
        // Params
        #[derive(Serialize, Deserialize)]
        struct Params {
            $(pub $name: $type),*
        }

        // The actual function (wont be accessable from the outside due to hygiene)
        fn serverside($($name:$type),*) -> $ret $body

        // Endpoint
        #[post($path, data="<body>")]
        fn $apiname(body: String) -> String {
            let data: Params = serde_json::from_str(&body).unwrap();
            let result = serverside($(data.$name),*);
            let response = serde_json::to_string(&result).unwrap();
            response
        }

        // HTTP-Request
        async fn $fn($($name:$type),*) -> $ret  {
            let client = reqwest::Client::new();
            let params = Params { $($name),* };
            let response = client
                .post(format!("http://localhost:8000{}", $path))
                .body(serde_json::to_string(&params).unwrap())
                .send().await
                .unwrap();
            let mut body = response.text().await.unwrap();
            let data: $ret = serde_json::from_str(&body).unwrap();
            data
        }
    };
}
```

Be aware that you will need to check/modify the hardcoded url in the macro to make it work on your machine!

I would love to hear your opinion on this :)
Have a nice day!