dev-template/README.md
Antoine Pelletier 572489ed5c first commit
2026-07-29 16:47:05 +02:00

156 lines
6.3 KiB
Markdown

# 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<Domain>` 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/<timestamp>_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 <name>`.
## 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.