Getting Started

If you're looking to start writing simple, productive, safe and fast web backends , you've come to the right place.

What This Book Covers

This book aims to be a comprehensive, up-to-date guide to using the web framework darpi and the crates associated with it, appropriate for beginners and old hands alike.

A demo app to check out

Let's go ahead!

Why darpi?

We all love how Rust allows us to write fast, safe software. darpi attempts at extending that and having stronger invariants proven that are specific to the web.

Performance, safety and simplicity are the main goals the framework is trying to achieve.

Applications built with darpi have the potential to be much faster and use fewer resources than other frameworks. To achieve its goals, darpi utilizes macros, heavily.

The State of Rust web frameworks

While some great web frameworks are out there, there is room for another point of view. Some of them are either not very flexible, too complicated, pushed compile time errors to runtime by using the type std::any::Any or all of the above.

With continuation of the Rust way, we would have to extend the Rust guarantees:

  • Outstanding runtime performance for concurrent workloads.
  • Flexible, testable and composable software.
  • Minimizing runtime errors, as much as possible.
  • Simplicity.

Basics

darpi provides various primitives to build web servers and applications with Rust. It provides routing, middleware, pre-processing of requests, post-processing of responses, etc.

darpi uses shaku for statically verifiable dependency injection, since it is alligned with the goals darpi has.

It is important to note that darpi does not store dynamic information about the application. Everything is achieved by code generation. For example, the provided routes are represented by an enum variants used in a match statement.

darpi solves conflicting paths by sorting, based on different of factors such as number of arguments in a path and their position within the string.

/user/{name} and /user/article are conflicting, if a user's name happens to be article. Therefore, the more generic path should be matched later.

App

The app macro generates code that link all the other components together.

#[tokio::main]
async fn main() -> Result<(), darpi::Error> {
    let address = format!("127.0.0.1:{}", 3000);
    app!({
        address: address,
        container: {
            factory: make_container(),
            type: Container
        },
        middleware: {
            request: [body_size_limit(128), decompress()],
            response: []
        },
        jobs: {
            request: [],
            response: [first_sync_job, first_sync_job1, first_sync_io_job]
        },
        handlers: [
            {
                route: "/",
                method: Method::GET,
                handler: home
            },
        ]
    })
    .run()
    .await
}

Lets break it down.

address can be either a String or a &'static str

container has a factory function, which is used to create a shaku container and a type (supports arguments too), which is the return type of the factory.

middleware, jobs and handlers we will tackle in the next chapters.

Handlers

The handler macro has 3 optional arguments in a json like format. Trailing commas are not supported as of now.


#![allow(unused)]
fn main() {
#[handler({
    container: Container,
    middleware: {
        request: [],
        response: []
    },
    jobs: {
        request: [],
        response: []
    }
})]
}

A request handler is an async function that accepts zero or more arguments. The arguments can be provided in several ways, by macro attributes. Handlers bound by a GET method do not have access to a request body. The return type must impl darpi::response::Responder. For convenience, it is implemented for all common types.

possible arguments

  • dependency injection container
    • #[inject] my_arg: Arc<dyn SomeTrait>
  • request
    • #[request] r: darpi::Request<darpi::Body> consumes the entire request therefore prevents other attributes from being used except #[path]
    • #[request_parts] rp: &darpi::RequestParts
    • #[query] q: MyStruct where MyStruct has to implement serde::Deserialize. Furthermore, if wrapped in an Option it is not mandatory.
    • #[path] path: MyStruct where MyStruct has to implement serde::Deserialize. /user/{user_id}/article/{article_id} will deserialize both ids into MyStruct.
    • #[body] data: impl FromRequestBody<T: serde:Deserialize> if handler is not linked to a GET request
  • middleware
    • #[middleware::request(i) my_arg: T] where i is a literal index of the middleware linked to the handler
    • #[middleware::response(i) my_arg: T] does not exist (obviously), because response middleware runs after the handler returns

While i recommend using the macros. If you really want to, you could implement the handler trait.


#![allow(unused)]
fn main() {
use darpi_middleware::body_size_limit;
use darpi::{app, handler, response::Responder, Method, Path, Json, Query};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Path, Query)]
pub struct Name {
    name: String,
}

#[handler({
    middleware: {
        // roundtrip returns Result<String, Error>
        // later we can access it via #[middleware::request(0)]
        request: [roundtrip("blah")]
    }
})]
async fn do_something(
  #[request_parts] _rp: &RequestParts,
  // the request query is deserialized into Name
  // if deseriliazation fails, it will result in an error response
  // to make it optional wrap it in an Option<Name>
  #[query] query: Name,
  // the request path is deserialized into Name
  #[path] path: Name,
  // the request body is deserialized into the struct Name
  // it is important to mention that the wrapper around Name
  // should implement darpi::request::FromRequestBody
  // Common formats like Json, Xml and Yaml are supported out
  // of the box but users can implement their own
  #[body] payload: Json<Name>,
  // we can access the T from Ok(T) in the middleware result
  #[middleware::request(0)] m_str: String, // returning a String works because darpi has implemented
  // the Responder trait for common types
) -> String {
  format!(
    "query: {:#?} path: {} body: {} middleware: {}",
    query, path.name, payload.name, m_str
  )
}
}

An example returning a Response object. For more information visit

response

status codes

body


#![allow(unused)]
fn main() {
use darpi::{handler, Body, Response, StatusCode};

#[handler]
async fn my_handler() -> Response<Body> {
    Response::builder()
        .status(StatusCode::OK)
        .body(Default::default())
        .unwrap()
}
}

errors

Any error implementing the ResponderError trait can be used in a handler return type. In this case, we are using the default implementation of the trait.

use darpi::response::ResponderError;

#[derive(Display, Debug)]
pub enum Error {
    #[display(fmt = "wrong credentials")]
    WrongCredentialsError,
    #[display(fmt = "jwt token not valid")]
    JWTTokenError,
    #[display(fmt = "jwt token creation error")]
    JWTTokenCreationError,
    #[display(fmt = "no auth header")]
    NoAuthHeaderError,
    #[display(fmt = "invalid auth header")]
    InvalidAuthHeaderError,
    #[display(fmt = "no permission")]
    NoPermissionError,
}

impl ResponderError for Error {}

#[derive(Deserialize, Serialize, Debug)]
pub struct Login {
    email: String,
    password: String,
}

#[handler({
    container: Container
})]
async fn login(
    #[body] data: Json<Login>,
    #[inject] jwt_tok_creator: Arc<dyn JwtTokenCreator>,
) -> Result<Token, Error> {
    //verify user data
    let admin = Role::Admin; // hardcoded just for the example
    let uid = "uid"; // hardcoded just for the example
    let tok = jwt_tok_creator.create(uid, &admin).await?;
    Ok(tok)
}

middleware

darpiā€™s middleware system allows us to add additional behavior to request/response processing. Middleware can hook into an incoming request process, enabling us to halt request processing to return a response early and modify the request body. Middleware can also hook into response processing.

middleware types

  • global (app)
    • Request
    • Response
  • local (handler)
    • Request
    • Response

possible arguments

  • Request middleware

    • dependency injection container
      • #[inject] my_arg: Arc<dyn SomeTrait>
    • request
      • #[request] rp: &mut darpi::Request<darpi::Body> supports shared and mutable reference
    • handler
      • #[handler] arg: T where the value must be provided when the middleware is invoked
  • Response middleware

    • dependency injection container
      • #[inject] my_arg: Arc<dyn SomeTrait>
    • response
      • #[response] r: &mut darpi::Response<Body> supports shared and mutable reference
    • handler
      • #[handler] arg: T where the value must be provided when the middleware is invoked

While i recommend using the macros. If you really want to, you could implement the middleware request and response traits.


#![allow(unused)]
fn main() {
use darpi::{middleware, request::PayloadError, Body, HttpBody};

#[middleware(Request)]
pub async fn body_size_limit(
  #[request] r: &Request<Body>,
  #[handler] size: u64,
) -> Result<(), PayloadError> {
  if let Some(limit) = r.size_hint().upper() {
    if size < limit {
      return Err(PayloadError::Size(size, limit));
    }
  }
  Ok(())
}
}

a more complicated example


#![allow(unused)]
fn main() {
#[middleware(Request)]
pub async fn authorize(
  #[handler] role: impl UserRole,
  #[request] rp: &Request<Body>,
  #[inject] algo_provider: Arc<dyn JwtAlgorithmProvider>,
  #[inject] token_ext: Arc<dyn TokenExtractor>,
  #[inject] secret_provider: Arc<dyn JwtSecretProvider>,
) -> Result<Claims, Error> {
  let token_res = token_ext.extract(&rp).await;
  match token_res {
    Ok(jwt) => {
      let decoded = decode::<Claims>(
        &jwt,
        secret_provider.decoding_key().await,
        &Validation::new(algo_provider.algorithm().await),
      ).map_err(|_| Error::JWTTokenError)?;

      if !role.is_authorized(&decoded.claims) {
        return Err(Error::NoPermissionError);
      }

      Ok(decoded.claims)
    }
    Err(e) => return Err(e),
  }
}
}

Jobs

Jobs represent some work packaged up in a function that the application has to execute but it's not necessary to hold up the response to the user. Just like middleware, jobs are bound to a Request or a Response and utilize the same argument system as middleware.

We have 3 types of jobs based on their behaviour. This is necessary to have an optimal way of using our resources.

  • Job
    • Future that does not block and does not perform any significant intensive work . It is queued on the regular tokio runtime.
    • CpuBound is a type of function that would do heavy computation and does not benefit from the tokio runtime. We would execute those on the Rayon runtime that is optimized for that.
    • IOBlocking is suitable for various kinds of IO operations that cannot be performed asynchronously. It is executed on a separate tokio blocking threads.

While i recommend using the macros. If you really want to, you could implement the Job factory request and response traits.


#![allow(unused)]
fn main() {
pub enum Job<T = ()> {
    Future(FutureJob<T>),
    CpuBound(CpuJob<T>),
    IOBlocking(IOBlockingJob<T>),
}

pub struct FutureJob<T = ()>(Pin<Box<dyn Future<Output = T> + Send>>);
pub struct CpuJob<T = ()>(Box<dyn FnOnce() -> T + Send>);
pub struct IOBlockingJob<T = ()>(Box<dyn FnOnce() -> T + Send>);

}

Few short examples.


#![allow(unused)]
fn main() {
#[job_factory(Request)]
async fn first_async_job() -> FutureJob {
  async { println!("first job in the background.") }.into()
}
}

// blocking here is ok!


#![allow(unused)]
fn main() {
#[job_factory(Response)]
async fn first_sync_job(#[response] r: &Response<Body>) -> IOBlockingJob {
  let status_code = r.status();
  let job = move || {
    std::thread::sleep(std::time::Duration::from_secs(2));
    println!(
      "first_sync_job in the background for a request with status {}",
      status_code
    );
  };
  job.into()
}
}

#![allow(unused)]
fn main() {
#[job_factory(Response)]
async fn my_heavy_computation() -> CpuJob {
  let job = || {
    for _ in 0..100 {
      let mut r = 0;
      for _ in 0..10000000 {
        r += 1;
      }
      println!("my_heavy_computation runs in the background. {}", r);
    }
  };
  job.into()
}
}

Web sockets


#![allow(unused)]
fn main() {
use darpi::futures::{SinkExt, StreamExt};
use darpi::{app, handler, job::FutureJob, response::UpgradeWS, Body, Method, Request};
use tokio_tungstenite::{tungstenite::protocol::Role, WebSocketStream};

#[handler]
async fn hello_world(#[request] r: Request<Body>) -> Result<UpgradeWS, String> {
    let resp = UpgradeWS::from_header(r.headers())
        .ok_or("missing SEC_WEBSOCKET_KEY header".to_string())?;

    FutureJob::from(async move {
        let upgraded = darpi::upgrade::on(r).await.unwrap();
        let mut ws_stream = WebSocketStream::from_raw_socket(upgraded, Role::Server, None).await;

        while let Some(msg) = ws_stream.next().await {
            let msg = match msg {
                Ok(m) => m,
                Err(e) => {
                    println!("error trying to receive:  `{:#?}`", e);
                    return;
                }
            };

            if msg.is_text() || msg.is_binary() {
                println!("received a message `{}`", msg);
                if let Err(e) = ws_stream.send(msg).await {
                    println!("error trying to send:  `{:#?}`", e);
                    return;
                }
            } else if msg.is_close() {
                println!("closing websocket");
                return;
            }
        }
    })
        .spawn()
        .map_err(|e| format!("{}", e))?;

    Ok(resp)
}
}

Advanced

In this chapter, we will learn how we can extend the framework with our own custom implementations.

Extractors

As we learned from the Handlers chapter, we can get a request body by declaring #[body] payload: Json<Name> in the handler arguments.

Extractors are a way to get a request body in a certain format.

While the framework provides Json, Xml and Yaml extractor implementations, you might need to implement your own.

To do that, we need to implement the FromRequestBody or FromRequestBodyWithContainer trait, depending on whether we need to use the dependency injection container.

Lets start with FromRequestBody. The trait definition is as follows. As you can see, we have 2 generic types. If in any case, an error is returned from the trait methods, the request processing will immediately stop and an error response will be returned to the client. This is the reason the E type has ResponderError bounds.


#![allow(unused)]
fn main() {
#[async_trait]
pub trait FromRequestBody<T, E>
where
    T: de::DeserializeOwned + 'static,
    E: ResponderError + 'static,
{
    async fn assert_content_type(_content_type: Option<&HeaderValue>) -> Result<(), E> {
        Ok(())
    }
    async fn extract(headers: &HeaderMap, b: Body) -> Result<T, E>;
}
}

As the implementor, we need to decide whether we want to require the content type header or not. You can choose to not have it as a requirement from the client, then you can simply not implement it and use the default implementation. And finally, the extract method is the thing that does most of the work. You can very well imagine that a basic implementation of the would be as follows.


#![allow(unused)]
fn main() {
use darpi::hyper::body;
pub struct MyFormat<T>(pub T);

#[async_trait]
impl<T> FromRequestBody<MyFormat<T>, MyErr> for MyFormat<T>
    where
        T: DeserializeOwned + 'static,
{
    async fn assert_content_type(content_type: Option<&HeaderValue>) -> Result<(), MyErr> {
        if let Some(hv) = content_type {
            if hv != "application/my_format" {
                return Err(MyErr::InvalidContentType);
            }
            return Ok(());
        }
        Err(MyErr::MissingContentType)
    }
    async fn extract(_: &HeaderMap, b: Body) -> Result<MyFormat<T>, MyErr> {
        let full_body = body::to_bytes(b).await?;
        // here is where we need to deserialize the full_body bytes
        // for the example, serde_json is used.
        let ser: T = serde_json::from_slice(&full_body)?;
        Ok(MyFormat(ser))
    }
}

#[derive(Display)]
pub enum MyErr {
    ReadBody(hyper::Error),
    Serde(Error),
    InvalidContentType,
    MissingContentType,
}

impl From<Error> for MyErr {
    fn from(e: Error) -> Self {
        Self::Serde(e)
    }
}

impl From<hyper::Error> for MyErr {
    fn from(e: hyper::Error) -> Self {
        Self::ReadBody(e)
    }
}

impl ResponderError for MyErr {}
}

If you have dependencies that you need to access, you can instead implement FromRequestBodyWithContainer. The implementation of the methods is exactly the same as above. The only difference is the C generic type that you need to specify the relevant bounds.


#![allow(unused)]
fn main() {
#[async_trait]
impl<F, T, E, C> FromRequestBodyWithContainer<T, E, C> for F
where
    F: FromRequestBody<T, E> + 'static,
    T: de::DeserializeOwned + 'static,
    E: ResponderError + 'static,
    C: std::any::Any + Sync + Send,
{
    async fn assert_content_type(content_type: Option<&HeaderValue>, _: Arc<C>) -> Result<(), E> {
        F::assert_content_type(content_type).await
    }
    async fn extract(headers: &HeaderMap<HeaderValue>, b: Body, _: Arc<C>) -> Result<T, E> {
        F::extract(headers, b).await
    }
}
}

As an example, we can see the graphql integration implements this trait. As you can see the line let opts = container.resolve().get(); is how you can fetch the specified dependency.


#![allow(unused)]
fn main() {
pub trait MultipartOptionsProvider: Interface {
    fn get(&self) -> MultipartOptions;
}

#[derive(Component)]
#[shaku(interface = MultipartOptionsProvider)]
pub struct MultipartOptionsProviderImpl {
    opts: MultipartOptions,
}

impl MultipartOptionsProvider for MultipartOptionsProviderImpl {
    fn get(&self) -> MultipartOptions {
        self.opts.clone()
    }
}

#[async_trait]
impl<C: 'static> FromRequestBodyWithContainer<GraphQLBody<BatchRequest>, GraphQLError, C>
for GraphQLBody<BatchRequest>
    where
        C: HasComponent<dyn MultipartOptionsProvider>,
{
    async fn extract(
        headers: &HeaderMap,
        mut body: darpi::Body,
        container: Arc<C>,
    ) -> Result<GraphQLBody<BatchRequest>, GraphQLError> {
        let content_type = headers
            .get(http::header::CONTENT_TYPE)
            .and_then(|value| value.to_str().ok())
            .map(|value| value.to_string());

        let (tx, rx): (
            Sender<std::result::Result<Bytes, _>>,
            Receiver<std::result::Result<Bytes, _>>,
        ) = bounded(16);

        darpi::job::FutureJob::from(async move {
            while let Some(item) = body.next().await {
                if tx.send(item).await.is_err() {
                    return;
                }
            }
        })
            .spawn()
            .map_err(|e| GraphQLError::Send(e.to_string()))?;

        let opts = container.resolve().get();
        Ok(GraphQLBody(BatchRequest(
            async_graphql::http::receive_batch_body(
                content_type,
                rx.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
                    .into_async_read(),
                opts,
            )
                .await
                .map_err(|e| GraphQLError::ParseRequest(e))?,
        )))
    }
}
}