Modern e-commerce playground with a Rust backend (Axum, SQLx, Redis) and a Next.js 15 frontend.
- Backend:
api-server/(Rust, Axum, Postgres, Redis) - Frontend:
web/(Next.js 15, React 19, Tailwind)
EcoCart/
api-server/ # Rust API (Axum), Docker, SQLx migrations, docs
web/ # Next.js frontend
- Docker and Docker Compose
- Node.js 18+ and npm (for local frontend)
- Rust toolchain (if you want to run the backend locally instead of Docker)
- Copy example env:
cd api-server
cp example.env .env- Key variables (edit as needed):
# --- Database ---
DATABASE_URL=postgres://ecocart:password@localhost:5432/ecocart?sslmode=disable
DB_HOST=localhost
DB_PORT=5432
DB_USER=ecocart
DB_PASSWORD=password
DB_NAME=ecocart
DB_SSLMODE=disable
# --- System ---
APP_ENV=development
LOG_LEVEL=info
API_HOST=127.0.0.1
API_PORT=8080
GRPC_HOST=127.0.0.1
GRPC_PORT=50051
SHUTDOWN_GRACE_SECONDS=10
# --- Auth/JWT ---
JWT_SECRET=change-me-in-production
JWT_ISSUER=ecocart
JWT_ACCESS_TTL_SECONDS=900
JWT_REFRESH_TTL_SECONDS=2592000
# --- Redis ---
REDIS_URL=redis://127.0.0.1:6379/0
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=
REDIS_DB=0
REDIS_TLS=false
# --- Seed (Admin) ---
# Used by the `ecocart seed` command to create/promote an admin
ADMIN_USERNAME=admin
[email protected]
ADMIN_PASSWORD=admin123Notes:
- Docker Compose overrides some values (e.g.,
API_HOST=0.0.0.0, DB/Redis hosts set to service names inside the network). - Change
JWT_SECRETfor any non-local usage.
Create an env file that points the app to the backend:
cd web
printf "API_BASE_URL=http://127.0.0.1:8080\n" > .env.localThis is read in web/src/lib/config.ts.
From the repo root (Compose file lives here):
# 1) Start Postgres, Redis, API, and Web (builds images on first run)
docker compose up --build -d
# 2) Tail API logs
docker compose logs -f api
# 3) Stop stack
docker compose down- Web app:
http://localhost:3000 - API base URL:
http://localhost:8080 - Health check:
GET http://localhost:8080/api/health - Static uploads:
http://localhost:8080/uploads/... - Swagger UI:
http://localhost:8080/docs - OpenAPI JSON:
http://localhost:8080/api-docs/openapi.json
In this setup, the API container runs migrations and seeds a default admin before starting the server.
The admin account is created by the seed command using values from api-server/example.env (or your .env):
ADMIN_USERNAME=admin
[email protected]
ADMIN_PASSWORD=admin123Ways to access admin:
-
Frontend UI:
- Visit
http://localhost:3000/auth/login - Log in using the admin email/password above
- Then navigate to
http://localhost:3000/admin
- Visit
-
API directly:
POST http://localhost:8080/api/auth/login- JSON body:
{ "email": "[email protected]", "password": "admin123" } - Use the returned
access_tokenasAuthorization: Bearer <token>for admin-only endpoints (e.g., creating categories).
To seed manually when running locally, run:
cd api-server
cargo run -- seedThe seed is idempotent: if the user exists it will be promoted to admin and the password rotated; otherwise it will be created.
You have two options:
- Recommended (sqlx-cli):
cd api-server
cargo install sqlx-cli --no-default-features --features rustls,postgres
DATABASE_URL=postgres://ecocart:password@localhost:5432/ecocart?sslmode=disable \
sqlx migrate run- Project command:
cd api-server
DATABASE_URL=postgres://ecocart:password@localhost:5432/ecocart?sslmode=disable \
make migrateTip: When the stack is up via Docker, Postgres is exposed on localhost:5432, so you can run migrations from your host using the provided DATABASE_URL.
cd api-server
# 1) Run migrations (DATABASE_URL must point to your Postgres)
cargo run -- migrate
# 2) Seed admin (uses ADMIN_* from .env)
cargo run -- seed
# 3) Start the API server
cargo run -- ecocart-apis
# Or hot-reload (requires cargo-watch)
make devUseful targets (api-server/Makefile): make run, make test, make fmt, make clippy, make docs.
Rust API docs are generated from Markdown in api-server/docs/ and embedded via rustdoc.
cd api-server
make docs # runs: cargo doc --no-deps --openThe docs will open in your browser (target/doc/ecocart/index.html).
- Swagger UI:
http://127.0.0.1:8080/docs - Raw OpenAPI JSON:
http://127.0.0.1:8080/api-docs/openapi.json
The API uses utoipa to generate OpenAPI schemas and utoipa-swagger-ui to serve the interactive docs.
How to document endpoints:
- Derive schemas for request/response types using
ToSchema(and query params withIntoParams):
use utoipa::{ToSchema, IntoParams};
#[derive(serde::Deserialize, ToSchema)]
pub struct CreateThing { name: String }
#[derive(serde::Serialize, ToSchema)]
pub struct Thing { id: i32, name: String }
#[derive(serde::Deserialize, IntoParams)]
pub struct ThingQuery { page: Option<i64> }- Annotate handlers with
#[utoipa::path(...)]:
#[utoipa::path(
post,
path = "/api/things",
request_body = CreateThing,
responses((status = 200, body = Thing)),
tag = "Things"
)]
async fn create_thing(Json(req): Json<CreateThing>) -> AppResult<Json<Thing>> { /* ... */ }- Add new paths or schemas to the central OpenAPI in
api-server/src/interfaces/http/swagger.rsunder the#[openapi(paths(...), components(schemas(...)))]macro if they are not auto-discovered.
Notes:
- Auth-protected endpoints can declare security via
security(("bearerAuth" = []))in the#[utoipa::path]macro. - Common error payload:
ApiErrorResponse(already exported to the OpenAPI components).
cd web
npm ci
npm run dev
# App: http://localhost:3000Build and start production server:
cd web
npm run build
npm startEnsure web/.env.local contains:
API_BASE_URL=http://127.0.0.1:8080- Root & health:
GET /→ API rootGET /api/health→ health probe
- Auth:
POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/refreshPOST /api/auth/logout
- Users:
GET /api/users/:id
- Categories:
GET /api/categoriesPOST /api/categories(admin)GET /api/categories/:idPUT /api/categories/:id(admin)DELETE /api/categories/:id(admin)
- Products:
GET /api/productsPOST /api/productsGET /api/products/:idPUT /api/products/:idDELETE /api/products/:idPOST /api/products/upload→ multipart upload; responses return/uploads/<file>URL
- Static files:
GET /uploads/*→ static file server for uploaded assets
- CORS is permissive in dev; rate limiting defaults to ~100 requests per 60 seconds.
- If the API container starts before Postgres/Redis are healthy, Compose will delay the API until they are ready.
- If migrations fail, verify
DATABASE_URLand that Postgres is reachable (docker compose psanddocker compose logs postgres). - On Apple Silicon, Docker images are multi-arch; no special flags should be required.
make dev— run with hot-reloadmake run— run the selected subcommand (CMD), default isecocart-apismake migrate— run SQLx migrations (requiresDATABASE_URL)make test/make fmt/make clippymake docs— build and open rustdoc
npm run dev— local dev server (Next.js)npm run build— production buildnpm start— start production server
Happy Coding!
Morshedul Islam Munna Software Engineer Email: [email protected]