CODING STANDARDS
VietGara — Backend Go · Owner Web and Admin Web React/TS · Mobile Flutter
Version 1.4 — October 7, 2026 · Cloud: Google Cloud Platform (GCP) · Backend: Go
1. Introduction
This document defines the coding standards applied across the entire VietGara codebase: Backend (Go — ADR-003), Owner Web and Admin Web (React + TypeScript — HLD Section 5; both follow Section 3), Owner App, Admin App, Customer App & Technician App (Flutter/Dart — HLD Section 5, SAD-C4 Section 3.1). Goal: readable, maintainable code that is consistent across team members/teams, reduces review time, and can be quality-checked automatically in CI (see Deployment Architecture, Section 3).
General principle: every rule in this document should be enforced automatically by tooling (linter/formatter/CI check) rather than relying solely on manual review — humans review business logic and architecture; machines check style and basic errors.
2. Coding Standards — Backend (Go)
2.1. Project Structure
Follow the standard package structure based on Clean Architecture described in the Low-Level Design (LLD), Section 2: domain (pure business logic) → usecase → repository → handler/grpc → middleware. The domain layer must never import framework/database packages.
2.2. Formatting & Naming Convention
- Running gofmt/gofumpt (
make fmt) before committing is mandatory — no debates over brace/whitespace style; let the tool decide. - Package names: lowercase, short, no underscores (e.g. billing, not billing_service).
- Exported (public) names: PascalCase. Unexported (private): camelCase.
- Name interfaces by behavior, preferring the -er suffix where it fits (e.g. EntitlementChecker, EventPublisher).
- Avoid ambiguous abbreviations (e.g. use garageID instead of gID); short loop variable names (i, err) are acceptable within a narrow scope.
// Goodfunc (s *SettlementService) CreateSettlement(ctx context.Context, req CreateSettlementRequest) (*Settlement, error)
// Bad: missing context, unclear namingfunc (s *SettlementService) Create(r Req) (*Settlement, error)2.3. Context & Error Handling
- context.Context is always the first parameter of any function performing I/O (database, gRPC, HTTP calls).
- Wrap errors with fmt.Errorf(”…: %w”, err) to preserve the error chain; use errors.Is/errors.As to check error types instead of string comparison.
- Define sentinel errors in the module’s
domainpackage (e.g.ErrInvitationExpired) and map them once, in the use case layer, topkg/apperrorerrors carrying a gRPC code and a stable reason (e.g.INVITATION_EXPIRED) plus field details for validation. The reason becomes the APIerror.codethe UI translates — never leak raw database errors into the API response. - Never use panic for ordinary business error flows — reserve it only for unrecoverable programming errors (e.g. invalid configuration at service startup).
if err != nil { return nil, fmt.Errorf("create settlement: %w", err)}
// domainvar ErrInvitationExpired = errors.New("invitation expired")
// usecase: one mapping table per modulecase errors.Is(err, domain.ErrInvitationExpired): return apperror.Wrap(apperror.CodeFailedPrecondition, err.Error(), err).WithReason(domain.ReasonInvitationExpired)
// validation with a field detailreturn apperror.Validation("fullName", apperror.IssueRequired, "full_name is required")2.4. Testing
- Write unit tests as table-driven tests (the Go community standard) for every usecase function with branching logic.
- Coverage:
make test-unitfails below 90% oninternal/...andpkg/...(generated mocks and*testhelper packages excluded); NFR-MT-02 sets 70% as the floor for core business logic. - Repositories/external clients are tested via interfaces + mocks generated by mockery (testify/mock,
make generate-mocks), or in-memory fakes (tenanttest,licensetest); SQL repositories are unit-tested with the scripteddbtestpgx fake and covered end-to-end by integration tests (build tagintegration, testcontainers) that run as thevietgara_approle so RLS applies. - Tests live in
_testpackages (thetestpackagelinter) and callt.Parallel()where possible. - Test files live alongside the source file with a test.go suffix; test functions are named Test
.
2.5. Logging & Security
- Use structured logging (
log/slog) — never fmt.Println in production code. - Never log sensitive data: passwords, tokens, card details, or customer personal data details (per Security Architecture, Section 7).
- Never hard-code secrets/API keys in code — always read from environment variables/Secret Manager (Infrastructure Architecture, Section 3).
2.6. gRPC/Protobuf
- .proto files live in the shared-contract repo
vietgara-proto, versioned by package name (e.g.vietgara.billing.v1); generated Go, gateway and OpenAPI code is committed and checked by CI. - Name RPCs as Verb+Noun following resource-oriented design:
List/Get/Create/Update/Delete<Resource>, or a specific verb for actions (AcceptInvitation,SubmitPlanPayment) — never generic names (Process, Handle). - Every RPC has a
google.api.httprule (API Specification, Section 1.1): resource in the body for Create/Update (body: "garage"),update_maskfor partial updates,repeated … data+Paginationfor lists,field_behavioron required and output-only fields, and enums starting with<ENUM>_UNSPECIFIED = 0. - Cross-module messages (pagination, errors) live in
vietgara.common.v1. - Every new RPC gets an entry in its module’s
Policiestable (public, unverified, user, admin or garage rule); a missing policy is rejected at runtime.
2.7. Required Tooling in CI
| Tool | Purpose |
|---|---|
| gofmt / gofumpt | Code formatting |
golangci-lint v2 (.golangci.yml) | Aggregated linting: staticcheck, errcheck, govet, gosec, revive, wsl, gocyclo, funlen, dupl, exhaustive, … |
go test -cover (make test-unit) | Run tests + measure coverage, fail below 90% |
make test-integration | Integration tests against real Postgres (RLS), Kafka and migrations |
| buf (proto repo) | lint, format, breaking-change detection |
3. Coding Standards — Owner Web and Admin Web (React + TypeScript)
3.1. Project Structure (feature-based)
src/ api/ -> schema.d.ts + openapi.json (generated), types.ts (aliases) app/ -> App (routes, code-split pages), AppShell (sidebar, header, palette), styles.css (3 lines: imports Tailwind and the kit) components/ -> thin app wrappers over the kit that add i18n, routing and API errors (Modal, FormModal, ConfirmDialog, QueryState, PageHeader, StatusBadge, SearchSelect, …) features/ auth/ -> session, sign-in/up, verification, password reset garages/ -> GarageContext (active garage, permissions), pages, api.ts staff/ roles/ invitations/ license/ admin/ customers/ vehicles/ partners/ user/ i18n/ -> vi.ts (source of the Messages type), en.ts, index.ts lib/ -> api client, errors, formatting, hooks test/ -> Vitest setup and render helpers3.2. TypeScript
- Enable strict mode in tsconfig.json; ban any (use unknown + type narrowing if a genuinely unknown type is needed).
- API types are generated from the proto-generated OpenAPI document (
npm run generate:api: swagger2openapi → openapi-typescript) and never written by hand;src/api/types.tsonly aliases them. CI fails whenschema.d.tsis stale. - All requests go through
lib/api.ts, which adds the bearer token, refreshes it once on 401, and throwsApiErrorwith the APIcodeand fielddetails.
3.3. Components & State Management
-
Write components as function components + hooks; separate logic (a custom hook) from presentation once a component exceeds ~150 lines.
-
Server-derived state is managed with TanStack Query: one
queryKeyfactory per resource (e.g.['garages', garageId, 'customers']),invalidateQueriesafter mutations,keepPreviousDatafor pagination — never copy API responses into local state. -
Pure UI state (modal open/closed, active tab) uses local useState/useReducer; app-wide client state uses React context (session, active garage). Add a store library only when context becomes insufficient.
-
Hide actions the user’s permissions do not allow (
useGarages().can('PERMISSION_…')), but never rely on it for security — the API enforces the same rules. -
Build screens from
@vietgara/web-ui, never from raw elements and classes:Button,Field+Input/NativeSelect/Textarea/Checkbox,Dialog,Table(with alabelon every cell so it stacks on phones),Badge,PageLayout+PageHeader,Panel. Do not make every section a card; useSectionfor titled groups without a box. -
Every data screen implements its states:
QueryState(skeleton / error with retry),EmptyStatewith the next action, no-results with Clear filters, and atoastafter a successful action. -
Controls get accessible names (icon-only buttons use
IconButtonwith a label that names the row, e.g. “Edit Nguyen Van A”); never convey state by color alone. -
Animation: CSS transitions for hover/focus; Motion (
m,PageTransition) only for route and layout changes; honorprefers-reduced-motion(the kit’sUIProviderdoes).
3.4. Internationalization
- Every user-visible string goes through react-i18next (
t('module.key')); Vietnamese (vi.ts) is the default and defines the message type, English (en.ts) must have the same keys (a type error otherwise). - API errors are shown with
errorText(t, error), which translateserrors.<CODE>and falls back to the server message. - Dates and money are formatted with the active locale (
lib/format.ts).
3.5. Naming Convention
- Components: PascalCase, filename matching the component name (QuoteList.tsx).
- Custom hooks: start with use (useQuoteForm).
- Global constants: UPPER_SNAKE_CASE.
3.6. Styling and the UI kit
- All styling comes from the
@vietgara/web-uipackage (repovietgara-web-ui, spec:02-Design/v2/03_Design_System.md): Tailwind CSS v4 theme, semantic color tokens for the light and dark themes, type scale, radius, shadows and motion, plus the React components. The app’ssrc/app/styles.cssonly imports Tailwind, the kit’s stylesheet and an@sourcefor the kit. - No literal colors, radii, font sizes or one-off component styles in an app. If a token or component is missing, change the kit: add the component with all its states to the gallery, run
npm test(the token contrast test), tagvX.Y.Z, then bump the tag in the apps. Tailwind utilities for layout (flex,gap-*, responsive prefixes) are fine in pages. - The kit is source-only TypeScript installed as a git dependency pinned by tag over HTTPS (
git+https://github.com/viettechno/vietgara-web-ui.git#vX.Y.Z);vite.config.tsexcludes it from pre-bundling (optimizeDeps.exclude). The lockfile must record thegit+httpsURL; Docker builds installgit. - The shell’s collapsible sidebar and its remembered state come from the kit (
AppShell); an app only supplies the sidebar contents and thecollapse/expandlabels, and usesuseSidebarCompact()for anything a collapsed rail hides. - Themes:
UIProvider(theme, tooltips, reduced motion, toasts) wraps the app;index.htmlcarries the no-flash theme script.
3.7. Testing & Linting
| Tool | Purpose |
|---|---|
| ESLint (typescript-eslint, react-hooks) | Lint TypeScript/React |
Prettier (.prettierrc.json) | Automatic code formatting (npm run format:check in CI) |
tsc -b | Type checking |
| Vitest + React Testing Library + jsdom | Tests focused on user-facing behavior (src/test/render.tsx wraps UIProvider, the query client, session and router, and provides a JSON response helper) |
| Kit repo: Vitest token test, gallery build | vietgara-web-ui fails CI when any text/surface pair drops below 4.5:1 or a control/focus pair below 3:1 in either theme |
| E2E selectors | Prefer roles, labels and visible text (getByRole('option'), getByLabel); never styling classes, so a restyle does not break vietgara-e2e |
4. Coding Standards — Mobile (Flutter/Dart)
4.1. Project Structure (feature-first, similar to Backend)
/lib /features /repair_order /data -> repository implementation, local DB (sqflite/drift), API client /domain -> pure models, use cases /presentation -> screens, widgets, state (Riverpod providers) /shared /widgets -> reusable widgets (pulled from the Design System) /services -> sync engine, connectivity checker /core -> routing, theme, dependency injection4.1a. Shared Core (vietgara-flutter-core)
Code every app needs the same way lives once, in the Dart package vietgara_core (repository vietgara-flutter-core, public, no secrets). It contains:
- the REST client (bearer auth, refresh-token rotation, typed
ApiException) and token storage; - the session and the e-mail sign-in screens;
- shared widgets, formatting, validators, the theme;
- their vi/en strings (
CoreLocalizations, read throughcontext.coreL10n); - test support (
package:vietgara_core/testing.dart).
Each app keeps its feature screens, router, app-specific state and its own strings (context.l10n).
Apps pin a released tag over HTTPS:
vietgara_core: git: url: https://github.com/viettechno/vietgara-flutter-core.git ref: v0.1.0A change to the core is a pull request there, then a version tag (semantic versioning, CHANGELOG.md), then one pull request per app that bumps ref:. Never pin a branch. To try an unreleased change, use a local pubspec_overrides.yaml path override; it is gitignored.
Code moves into the core when a second app needs it. App-specific logic does not move in: the core’s sign-out clears only the core’s state, and each app clears its own state (e.g. the owner app’s selected garage) by watching the session.
4.2. Null Safety & Naming
- Sound null safety is mandatory (default since Dart 2.12+); minimize use of the ! (non-null assertion) operator, preferring explicit null handling.
- Classes: UpperCamelCase; variables/functions: lowerCamelCase; files: snake_case.dart — per Effective Dart.
4.2a. App Identifiers
The company domain viettechno.com is shared by several products. Each app’s store identifier is its reverse DNS: com.viettechno.<product>.<app>. The same value is used for the Android applicationId (and the Kotlin package of MainActivity) and the iOS bundle identifier. iOS test targets append .RunnerTests.
| Repository | Identifier |
|---|---|
vietgara-owner-app | com.viettechno.vietgara.owner |
vietgara-admin-app | com.viettechno.vietgara.admin |
vietgara-customer-app | com.viettechno.vietgara.customer |
vietgara-technician-app | com.viettechno.vietgara.technician |
Set it when the repo is created, for example with flutter create --org com.viettechno.vietgara --project-name <app>, then adjust it to the value above. An identifier can never change once the app is published on Google Play or the App Store.
4.2b. Theme and shared widgets
- Every app uses
buildTheme(Brightness)fromvietgara_core(v0.4.0+) forthemeanddarkThemeand follows the system theme. Colors, radii, type and component themes come from there; read status and plate colors withcontext.vg(VgColors), radii fromVgRadius. NoColor(0x…)orColors.*in an app for UI semantics. - Use the core widgets for repeating patterns:
StatusChip(icon + tone),PlateChipfor license plates,SectionCard,AsyncBody/PagedListView(skeleton loading),MessageView/ErrorView(illustration + one action),FormSheet. Fonts (Be Vietnam Pro, JetBrains Mono) are bundled by the core. - Touch targets are at least 44dp (the theme sets button and tab minimums); never rely on color alone for state.
4.3. State Management
- Use Riverpod for all state (simpler than Bloc at the app’s current scale, easier to test, good dependency-injection support for repositories).
4.4. Offline-First (especially critical for the Technician App)
- Local data is stored using drift (type-safe SQLite) exactly as designed in the Low-Level Design, Section 5.2 (the pending_sync table).
- Every data-writing API call goes through an intermediate Sync Repository layer — the UI never calls the HTTP client directly, avoiding offline logic scattered throughout the codebase.
4.5. Testing & Linting
| Tool | Purpose |
|---|---|
| dart analyze + package lints (effective_dart) | Standard Dart linting |
| flutter test (widget tests) | Behavior-focused UI testing, no real device required |
| mocktail | Mock repositories/API clients in unit tests |
5. Cross-Cutting Rules
5.1. Code Review Checklist (applies to every language)
- Business logic matches the Functional Requirements Document, not just “it runs”.
- There is a test case for the main flow and at least one error/edge case.
- No hard-coded secrets/API keys, no logging of sensitive data.
- For database changes: a migration script + a rollback script are included (Deployment Architecture, Section 6).
- For API changes: the corresponding API Specification has been updated.
5.2. Required Documentation per Repo/Service
- README.md: the service’s purpose, how to run it locally, required environment variables.
- CHANGELOG.md: written from the Conventional Commit messages of the release; generating it automatically is planned (see Git_Strategy.md, Section 6).
5.3. Link to the Git Workflow
Commit message, branch, and pull request conventions are specified in detail in the Git_Strategy.md document — every PR must pass all the tooling listed in Sections 2.7/3.6/4.5 before it can be merged.