The dream about avoiding API-Endpoints and API-Calls (is it worth it?)

codinghusi

Gerrit Weiermann

Posted on November 6, 2022

The dream about avoiding API-Endpoints and API-Calls (is it worth it?)

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:

// 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));
Enter fullscreen mode Exit fullscreen mode

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:

#[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])
}
Enter fullscreen mode Exit fullscreen mode

And here you've got the macro:

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
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
codinghusi
Gerrit Weiermann

Posted on November 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related