diff --git a/Cargo.lock b/Cargo.lock
index d3cc004e..452b8eac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -876,6 +876,7 @@ dependencies = [
name = "garage_api"
version = "0.7.0"
dependencies = [
+ "async-trait",
"base64",
"bytes 1.1.0",
"chrono",
diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml
index 5e96b081..1ba3fd2a 100644
--- a/src/api/Cargo.toml
+++ b/src/api/Cargo.toml
@@ -19,6 +19,7 @@ garage_table = { version = "0.7.0", path = "../table" }
garage_block = { version = "0.7.0", path = "../block" }
garage_util = { version = "0.7.0", path = "../util" }
+async-trait = "0.1.7"
base64 = "0.13"
bytes = "1.0"
chrono = "0.4"
diff --git a/src/api/generic_server.rs b/src/api/generic_server.rs
new file mode 100644
index 00000000..f543d092
--- /dev/null
+++ b/src/api/generic_server.rs
@@ -0,0 +1,209 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
+use async_trait::async_trait;
+
+use chrono::{DateTime, NaiveDateTime, Utc};
+use futures::future::Future;
+use futures::prelude::*;
+use hyper::header;
+use hyper::server::conn::AddrStream;
+use hyper::service::{make_service_fn, service_fn};
+use hyper::{Body, Method, Request, Response, Server};
+
+use opentelemetry::{
+ global,
+ metrics::{Counter, ValueRecorder},
+ trace::{FutureExt, SpanRef, TraceContextExt, Tracer},
+ Context, KeyValue,
+};
+
+use garage_util::error::Error as GarageError;
+use garage_util::metrics::{gen_trace_id, RecordDuration};
+
+use garage_model::garage::Garage;
+use garage_model::key_table::Key;
+
+use garage_table::util::*;
+
+use crate::error::*;
+use crate::signature::compute_scope;
+use crate::signature::payload::check_payload_signature;
+use crate::signature::streaming::SignedPayloadStream;
+use crate::signature::LONG_DATETIME;
+
+pub(crate) trait ApiEndpoint: Send + Sync + 'static {
+ fn name(&self) -> &'static str;
+ fn add_span_attributes<'a>(&self, span: SpanRef<'a>);
+}
+
+#[async_trait]
+pub(crate) trait ApiHandler: Send + Sync + 'static {
+ const API_NAME: &'static str;
+ const API_NAME_DISPLAY: &'static str;
+
+ type Endpoint: ApiEndpoint;
+
+ fn parse_endpoint(&self, r: &Request
) -> Result;
+ async fn handle(
+ &self,
+ req: Request,
+ endpoint: Self::Endpoint,
+ ) -> Result, Error>;
+}
+
+pub(crate) struct ApiServer {
+ s3_region: String,
+ api_handler: A,
+
+ // Metrics
+ request_counter: Counter,
+ error_counter: Counter,
+ request_duration: ValueRecorder,
+}
+
+impl ApiServer {
+ pub fn new(s3_region: String, api_handler: A) -> Arc {
+ let meter = global::meter("garage/api");
+ Arc::new(Self {
+ s3_region,
+ api_handler,
+ request_counter: meter
+ .u64_counter(format!("api.{}.request_counter", A::API_NAME))
+ .with_description(format!(
+ "Number of API calls to the various {} API endpoints",
+ A::API_NAME_DISPLAY
+ ))
+ .init(),
+ error_counter: meter
+ .u64_counter(format!("api.{}.error_counter", A::API_NAME))
+ .with_description(format!(
+ "Number of API calls to the various {} API endpoints that resulted in errors",
+ A::API_NAME_DISPLAY
+ ))
+ .init(),
+ request_duration: meter
+ .f64_value_recorder(format!("api.{}.request_duration", A::API_NAME))
+ .with_description(format!(
+ "Duration of API calls to the various {} API endpoints",
+ A::API_NAME_DISPLAY
+ ))
+ .init(),
+ })
+ }
+
+ pub async fn run_server(
+ self: Arc,
+ bind_addr: SocketAddr,
+ shutdown_signal: impl Future