Skip to main content

basic/
basic.rs

1use expressjs::express_state;
2use expressjs::prelude::*;
3use hyper::header::{self, HeaderValue};
4use local_ip_address::local_ip;
5use log::info;
6use serde_json::json;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9fn setup_logger() -> Result<(), Box<dyn std::error::Error>> {
10    use std::io::Write;
11    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
12        .format(|buf, record| {
13            writeln!(
14                buf,
15                "{} [{}] {}",
16                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
17                record.level(),
18                record.args()
19            )
20        })
21        .try_init()?;
22    Ok(())
23}
24
25#[derive(Debug, Default)]
26pub struct State {
27    request_count: AtomicU32,
28}
29
30express_state!(STATE, State);
31
32#[tokio::main]
33async fn main() {
34    setup_logger().unwrap();
35
36    const PORT: u16 = 9000;
37    let mut app = express();
38
39    // Middleware
40    app.use_global(NormalizePathMiddleware::new())
41        .use_global(CorsMiddleware::permissive())
42        .use_global(LoggingMiddleware)
43        .use_with("/css/{*p}", StaticServeMiddleware::new("."))
44        .use_with("/expressjs_tests/{*p}", StaticServeMiddleware::new("."));
45
46    // Routes
47
48    // GET / - index page
49    app.get("/", async |_req, res| {
50        let html = r#"
51        <!DOCTYPE html>
52        <html lang="en">
53        <head>
54            <meta charset="UTF-8">
55            <meta name="viewport" content="width=device-width, initial-scale=1.0">
56            <title>Welcome</title>
57            <link rel="stylesheet" href="/css/index.css"/>
58        </head>
59        <body>
60            <h1>Welcome to expressjs</h1>
61            <p>This is a minimal HTML page served from Rust.</p>
62            <ul>
63                <li><a href="/json">JSON Endpoint</a></li>
64                <li><a href="/redirect">Redirect to Home</a></li>
65                <li><a href="status">Trigger 400</a></li>
66                <li><a href="hello">Say Hello</a></li>
67            </ul>
68        </body>
69        </html>
70        "#;
71
72        res.status_code(200)
73            .content_type("text/html; charset=utf-8")
74            .send_html(html)
75    });
76
77    // GET /count - request counter
78    app.get("/count", async |_req, res| {
79        let count = STATE.request_count.fetch_add(1, Ordering::Relaxed);
80
81        res.send_json(&json!({
82            "request_count": count,
83            "message": "Request count retrieved successfully"
84        }))
85    });
86
87    // GET /json - static JSON response
88    app.get("/json", async |_req, res| {
89        res.header(
90            hyper::header::CACHE_CONTROL,
91            HeaderValue::from_static("public, max-age=3600"),
92        )
93        .send_json(&json!({
94            "message": "Hello from JSON!",
95            "status": "success",
96            "version": "1.0"
97        }))
98    });
99
100    // GET /redirect - redirect to /
101    app.get("/redirect", async |_req, res| res.redirect("/"));
102
103    // GET /status - always 400
104    app.get("/status", async |_req, res| {
105        res.status_code(400).send_text("400 Bad Request")
106    });
107
108    // GET /status/:status - echo status param
109    app.get("/status/{status}", async |req, res| {
110        let value = req.params().get("status").unwrap_or("unknown");
111        res.send_text(format!("Status is {value}"))
112    });
113
114    // GET /file - stream Cargo.lock (async, borrows res across await)
115    app.get("/file", async |_req, res| {
116        res.send_file("./Cargo.lock").await
117    });
118
119    // /hello - X-Powered-By middleware then response
120    // app.use_with("/hello", async |_req: &mut Request, res: &mut Response| {
121    //     res.header("x-powered-by", HeaderValue::from_static("DevYatsu"));
122    //     stop_res()
123    // });
124
125    app.get("/hello", async |_req, res| {
126        res.body("Hello, world!")
127            .header(
128                header::CACHE_CONTROL,
129                HeaderValue::from_static("public, max-age=86400"),
130            )
131            .header(header::CONTENT_TYPE, HeaderValue::from_static("text/html"))
132    });
133
134    // Route builder pattern - multiple methods on the same path
135    app.route("/api/v1/user")
136        .get(async |_req, res| res.send_text("Get User"))
137        .post(async |_req, res| res.send_text("Post User"));
138
139    // all() - matches every HTTP method
140    app.all("/ping", async |_req, res| res.send_text("pong"));
141
142    // Custom 404 handler
143    app.not_found(async |_req, res| {
144        res.status_code(404)
145            .send_text("Custom 404: Page not found!")
146    });
147
148    // Listen
149    app.listen(PORT, async |port| {
150        let local_ip = local_ip().unwrap();
151        info!("🚀 Server running!");
152        info!("📍 Local:   http://localhost:{port}/");
153        info!("🌐 Network: http://{local_ip}:{port}/");
154    })
155    .await
156}