The dream about avoiding API-Endpoints and API-Calls (is it worth it?)
Gerrit Weiermann
Posted on November 6, 2022
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));
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])
}
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(¶ms).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!
Posted on November 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.