Skip to content

Project Layout

Every Node.js project starts with package.json. In Rust the equivalent is Cargo.toml. It declares your crate name, version, and dependencies. Cargo is both the package manager and the build tool — no separate npm vs tsc vs ts-node.

flowchart TD
  root["my-api/ (Express/NestJS project)"] --> pkg["package.json — dependencies + scripts"]
  root --> tsc["tsconfig.json — TypeScript compiler config"]
  root --> src["src/"]
  src --> main["main.ts — entry point"]
  src --> appmod["app.module.ts — (NestJS) root module"]
  src --> users["users/"]
  users --> ctrl["users.controller.ts"]
  users --> svc["users.service.ts"]
  users --> mod["users.module.ts"]
  root --> dist["dist/ — compiled output"]
Express/NestJS project layout
flowchart TD
  root["my-api/ (Axum project)"] --> toml["Cargo.toml — dependencies + metadata (= package.json)"]
  root --> lock["Cargo.lock — lockfile (= package-lock.json)"]
  root --> src["src/"]
  src --> main["main.rs — entry point"]
  src --> routes["routes/"]
  routes --> rmod["mod.rs — route definitions"]
  src --> handlers["handlers/"]
  handlers --> husers["users.rs — handler functions"]
  src --> models["models.rs — serde structs (request/response types)"]
  src --> state["state.rs — shared AppState"]
  src --> errors["errors.rs — custom error types"]
Axum project layout
[package]
name = "my-api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "uuid"] }
uuid = { version = "1", features = ["v4", "serde"] }
thiserror = "1"

Compared to npm install express, you declare everything in one file and Cargo resolves the dependency graph, compiles everything, and links it — no separate build step.

use axum::Router;
use std::net::SocketAddr;
mod errors;
mod handlers;
mod models;
mod routes;
mod state;
#[tokio::main]
async fn main() {
// initialise structured logging
tracing_subscriber::fmt()
.with_env_filter("my_api=debug,tower_http=debug")
.init();
let state = state::AppState::new().await;
let app = routes::create_router(state);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
tracing::info!("Listening on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}

#[tokio::main] is a macro that wraps main in a Tokio async runtime — equivalent to Node’s built-in event loop starting automatically.

Node/TSRust/Axum
package.jsonCargo.toml
npm installcargo add / cargo build
ts-node src/main.tscargo run
tsc --buildcargo build --release
nodemoncargo-watch -x run
src/app.tssrc/main.rs
process.env.PORTstd::env::var("PORT")

Run locally — this is a project layout lesson. No runnable browser snippet applies here. Create the project with cargo new my-api and add dependencies with cargo add axum tokio serde serde_json.

What is the Rust equivalent of `package.json`?
What does `#[tokio::main]` do?
Which command runs an Axum project during development?