lilydocs
HTTP APIWebSocketConsumer
Getting started3 min read

Your first HTTP application

Define a controller, return a JSON response, and run your Lily application.

By the end of this page, you will have an application that returns JSON for GET /api/hello. The example runs over local HTTP.

Prepare the project#

lilyrs 0.1.0 is published on crates.io. The framework's manifest minimum and validated toolchain are Rust 1.96.1, with edition 2024. Create your application as an independent Cargo project:

Terminal
cargo new hello-lily

Update hello-lily/Cargo.toml as follows:

TOML
[package]
name = "hello-lily"
version = "0.1.0"
edition = "2024"
rust-version = "1.96.1"

[dependencies]
lilyrs = { version = "0.1.0", features = ["http-api"] }
serde = { version = "1", features = ["derive"] }

lilyrs enables no components by default. The http-api feature exposes the HTTP API under lilyrs::http_api. Controllers, middleware, guards, and DI types share this entry point; you do not need separate HTTP macro, derive, or registry dependencies. The DI guide explains how to define services.

When adding an integration, extend the feature list on the same lilyrs dependency, for example features = ["http-api", "postgresql"]. The feature is spelled http-api, while the Rust module is http_api. The facade re-exports the existing components; application lifecycle ownership and canonical API calls remain the same.

Controller and application#

Replace src/main.rs with the following:

first_app.rs
use std::sync::Arc;
use lilyrs::http_api::{
    controller, AppBuilder, Controller, ControllerInitError, ControllerTrait,
    Extensions, HttpApiError,
};
use serde::Serialize;

#[derive(Serialize)]
struct Hello { message: &'static str }

#[derive(Controller)]
#[base_path("/api")]
struct HelloController;

#[lilyrs::http_api::async_trait::async_trait]
impl ControllerTrait for HelloController {
    async fn new(_: Arc<Extensions>) -> Result<Self, ControllerInitError> {
        Ok(Self)
    }
}

#[controller]
impl HelloController {
    #[get("/hello")]
    async fn hello(&self) -> Result<Hello, HttpApiError> {
        Ok(Hello { message: "Merhaba Lily" })
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    lilyrs::http_api::tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?
        .block_on(async {
            AppBuilder::new("127.0.0.1:8080")
                .tls_disabled()
                .build().await?
                .start().await?;
            Ok::<(), Box<dyn std::error::Error>>(())
        })
}

#[base_path("/api")] sets the controller's shared path. Combined with #[get("/hello")], it produces /api/hello. Result<Hello, HttpApiError> converts the successful value to JSON. Controllers are discovered at link time; there is no add_controller call.

Run the application#

Terminal
cd hello-lily
cargo run

In another terminal:

Terminal
curl -i http://127.0.0.1:8080/api/hello

A successful response has status 200 and the following body:

JSON
{"message":"Merhaba Lily"}

Shut down the application#

Ctrl+C initiates shutdown. start().await returns when shutdown operations have completed. Every successful build must complete a start or close path; dropping the App value does not replace async cleanup.

Choosing Tokio. The example builds the runtime through the HTTP facade. If you prefer #[tokio::main], add a direct tokio dependency to your application with the macros and runtime features.

Sources for this page v0.1.0

This guide is based on the reviewed framework commit.

QUICK START
Documentation
Source code