Skip to main content
Lesson 3 of 12Beginner⏱ 5 mins

Understanding main.rs

The entry point of your application.

17% of path

Learning Objectives

  • Understand the async main function

##Reading a minimal Actix Web main.rs

Every Actix Web program has the same skeleton. Once you understand each line of it, you can read almost any Actix Web project. Here is the smallest useful server:

code_blocksrust
use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn index() -> impl Responder {
    HttpResponse::Ok().body("Hello from Actix Web!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(index))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#The imports

use actix_web::{...} brings in the building blocks. App describes your application, HttpServer runs it on the network, HttpResponse builds replies, and Responder is a trait that means "anything that can be turned into a response".

#The handler

A handler is an ordinary async function. The #[get("/")] attribute is a macro that registers it for GET requests to the path /. It returns impl Responder, so you are free to return a string, JSON, or a full HttpResponse.

#The entry point

Rust programs start in main, but a web server needs an async runtime to handle thousands of connections. The #[actix_web::main] attribute turns async fn main into a normal program that starts the Actix runtime (built on Tokio) and runs your code inside it. Without it, the compiler would refuse to compile an async fn main.

#The server and the app factory

HttpServer::new takes a closure. This is called an app factory: Actix calls it once for every worker thread, and each call builds a fresh App. That is why the closure uses || App::new() instead of building one shared app.

Inside, App::new().service(index) registers the handler. Finally:

  • .bind(("127.0.0.1", 8080))? opens the TCP socket. The ? returns early if the port is already taken.
  • .run() creates the server future.
  • .await actually runs it until you press Ctrl+C.

#Why `std::io::Result<()>`

Binding a socket can fail, so main returns an io::Result. If something goes wrong, the program exits and prints the error instead of silently doing nothing.

#A quick mental picture

Think of it as three layers: HttpServer listens for connections, each worker owns an App, and the App routes each request to the right handler.

Interactive Code Inspector

Select any highlighted line of code below to deeply inspect its mechanics, understand why it exists, and what happens if you change it.

src/main.rs
1#[actix_web::main]
2async fn main() -> std::io::Result<()> {
3 Ok(())
4}
Line 1
#[actix_web::main]

What it does

Initializes the Actix asynchronous runtime.

Why use it?

Rust's standard `main` cannot be `async`. This macro wraps your main function.

How it works underneath

It expands into a synchronous main function that blocks on the async runtime.

If you remove it

The compiler will throw an error saying `main` cannot be async.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Observe the per-worker app factory.

Starting Code
HttpServer::new(|| { println!("building an App"); App::new().service(index) })
Suggested Modification

Add the println! shown above and start the server.

Expected Result

You see "building an App" printed several times, once for each worker thread.

Explanation: The closure is the app factory. Actix runs it once per worker, which is why shared state must be created outside the closure and cloned in.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Forgetting #[actix_web::main] on main

Rust cannot run an async main function without a runtime attribute.

Incorrect Pattern
async fn main() -> std::io::Result<()> { /* ... */ }
Correct Pattern
#[actix_web::main] async fn main() -> std::io::Result<()> { /* ... */ }

Explanation: The attribute generates a normal fn main that builds the runtime and blocks on your async code.

!

Forgetting .await after run()

run() only builds a future. Nothing happens until it is awaited.

Incorrect Pattern
HttpServer::new(|| App::new()).bind(("127.0.0.1", 8080))?.run();
Correct Pattern
HttpServer::new(|| App::new()).bind(("127.0.0.1", 8080))?.run().await

Explanation: Futures in Rust are lazy. Without .await the server is created and immediately dropped, so the program exits at once.

Practical Exercise

Change the port and the greeting

Task

Make the server listen on port 3000 and return the text "Welcome to my server".

Requirements

  • Bind to 127.0.0.1 on port 3000
  • The GET / handler returns "Welcome to my server"

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Production services keep this exact skeleton and grow it: configuration loading before the server, shared database pools passed in with app_data, middleware via wrap, and a graceful shutdown timeout on the server.

Key Takeaways

  • #[actix_web::main] lets main be async by starting the Actix (Tokio) runtime.
  • HttpServer::new takes an app factory closure that runs once per worker thread.
  • bind opens the port and run().await keeps the server running.
  • Handlers are async functions returning something that implements Responder.

Finished reading?

Mark this lesson as done to track your progress and earn XP.