DEV Community

Gerrit Weiermann
Gerrit Weiermann

Posted on

1

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!

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

While many AI coding tools operate as simple command-response systems, Qodo Gen 1.0 represents the next generation: autonomous, multi-step problem-solving agents that work alongside you.

Read full post →

Top comments (1)

Collapse
 
danbars profile image
Dan Bar-Shalom

Of course you are right that boilerplate code should be abstracted away and not reinvented over and over...

There are many libraries/frameworks that help automating the wiring.
If you're in the RESTful world, there's OpenAPI standard and its vast ecosystem that allows you to easily build a server. Checkout openapi.tools

There's Google Protobuf, although less popular, also has its own ecosystem, and there are more.

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay