# App template Starting point for an AGEPoly web project: a **Rust** backend (axum + sqlx + aide) serving a **Vue 3** frontend (TypeScript + vue-query + shadcn-vue), on **PostgreSQL** with **dbmate** migrations. The template is deliberately almost empty: one table (`items`), one route (`GET /api/items`) and one page (home) that displays it. They exist to show how the layers fit together — rename them, or delete them once your own code is in place. ## What you get - Layered backend (api / core / services), see [Backend](#backend) - An OpenAPI document generated from the code, and **TypeScript types generated from it**: the frontend cannot call an endpoint that does not exist - i18n (fr/en) on both sides, localized strings stored as JSONB - `dev-db/`: postgres + adminer in docker for development - Tailwind 4 and the shadcn-vue components already vendored in `frontend/src/components/ui` - A single binary in production: the backend serves the built frontend There is **no authentication** in this template: add whatever the project needs. ## Bootstrap a new project ```bash cp -r template /path/to/my-project && cd /path/to/my-project git init # 1. Rename the crate and the database (app-template -> my-project) ./rename.sh my-project "My Project" && rm rename.sh # 2. Start the development database cd dev-db && docker compose up -d && cd .. psql -h localhost -U postgres -c 'CREATE DATABASE my_project' dbmate up psql "$(grep DATABASE_URL .env | cut -d= -f2-)" -f db/seed.sql # optional demo data # 3. Configure the app cp config.example.yml config.yml # 4. Run the backend (port 3000) cargo run # 5. Run the frontend (port 5000, proxies /api to the backend) cd frontend && npm install && npm run dev ``` Open http://localhost:5000. The api documentation is on http://localhost:3000/api/docs. `config.yml` is gitignored: it is where the secrets go. `config.example.yml` documents every key, keep it up to date. Every value can also come from the environment (`APP__POSTGRES__PASSWORD=...`), which is how the app is configured in production. ## Layout ``` ├── src/ backend │ ├── api/ http layer: routes, extractors, OpenAPI │ ├── core/ business logic, independent of axum and sqlx │ │ ├── controller/ what the app can do │ │ ├── models/ domain types │ │ └── repositories/ traits describing what the core needs from the storage │ ├── services/ implementations of the repositories (postgres/sqlx) │ └── utils/ configuration ├── db/ │ ├── migrations/ dbmate migrations │ ├── schema.sql dump regenerated by dbmate, do not edit by hand │ └── seed.sql development data ├── dev-db/ postgres + adminer for development └── frontend/ └── src/ ├── components/ shared components (ui/ = shadcn-vue) ├── views/ one component per route ├── router/ route table ├── services/ api/ (one file per domain area) + i18n ├── lib/api.d.ts generated from the backend, never edited by hand ├── utils/types.ts shorthands over the generated schemas └── locales/ fr.yml / en.yml ``` ## Backend Requests flow through three layers, each one only knowing the next: ``` api (axum handler) -> core/controller -> core/repositories -> services/database status codes business logic trait sqlx + postgres domain models ``` The point of the repository traits is that the core never depends on sqlx: you can add another implementation (a mock in tests, another storage) without touching the logic. Handlers stay thin — extract the controller, call it, map the error to a status code — and their OpenAPI documentation sits right next to them (`fn *_docs`). ### Adding an entity 1. `dbmate n create_things` and write the migration, then `dbmate up` 2. `src/core/models/thing.rs`: the domain types 3. `src/core/repositories/things_repository.rs`: the trait, added to `DatabaseRepository` 4. `src/services/database/things.rs`: the sqlx implementation 5. `src/core/controller/things.rs`: the logic, plus a `ThingsControllerError` if needed 6. `src/api/things.rs`: the handlers and their docs, mounted in `src/api/mod.rs` 7. `cd frontend && npm run openapi` to regenerate the types, then write the service and the view The `Item` entity follows exactly these steps: copy it. Request bodies are best kept separate from the domain models (an `api/models.rs` holding the `...Api` structs and their `Into` impls), so that the public contract does not change every time a domain model does. ### sqlx and compilation `query!`/`query_as!` check the sql against a **real database at compile time**, so the development database must be up and migrated for `cargo build` to work. `DATABASE_URL` is read from `.env`. To build without a database (CI, docker image), commit the offline data: ```bash cargo install sqlx-cli cargo sqlx prepare # writes .sqlx/, commit it ``` ### Migrations ```bash dbmate n add_something # creates db/migrations/_add_something.sql dbmate up # applies, and regenerates db/schema.sql dbmate rollback # undoes the last migration (write your `migrate:down`!) ``` ## Frontend ```bash npm run dev # dev server on :5000, /api proxied to the backend on :3000 npm run build # type-check + build into dist/ npm run type-check npm run lint npm run format npm run openapi # regenerate src/lib/api.d.ts from the running backend ``` Rules of thumb: - views never call `fetch`: they use a hook from `services/api/`, which returns a vue-query query or mutation. Caching, loading and error states come for free. - mutations update the cache in `onSuccess` so the ui reacts immediately. - texts live in `locales/*.yml` and are used through `$t('key')`, never hardcoded. - add a shadcn-vue component with `npx shadcn-vue@latest add `. ## Production `cargo build --release`, `npm run build`, then point `frontend_dir` at the built `frontend/dist`: the backend serves the static files and falls back on `index.html` so the vue router keeps working on a page reload. Only one process to deploy.