DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Gerrit Weiermann
Gerrit Weiermann

Posted on

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!

Top comments (0)

πŸ‘‹ Hey, my name is Noah and I’m the one who set up this ad. My job is to get you to join DEV, so if you fancy doing me a favor, I’d love for you to create an account.

If you found DEV from searching around, here are a couple of our most popular articles on DEV: