Project Layout
From package.json to Cargo.toml
Section titled “From package.json to Cargo.toml”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"]
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"]
Cargo.toml for an Axum service
Section titled “Cargo.toml for an Axum service”[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.
main.rs — the entry point
Section titled “main.rs — the entry point”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.
Key concepts
Section titled “Key concepts”| Node/TS | Rust/Axum |
|---|---|
package.json | Cargo.toml |
npm install | cargo add / cargo build |
ts-node src/main.ts | cargo run |
tsc --build | cargo build --release |
nodemon | cargo-watch -x run |
src/app.ts | src/main.rs |
process.env.PORT | std::env::var("PORT") |
Run locally — this is a project layout lesson. No runnable browser snippet applies here. Create the project with
cargo new my-apiand add dependencies withcargo add axum tokio serde serde_json.