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 (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.