Skip to content

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.
// Good
func (s *SettlementService) CreateSettlement(ctx context.Context, req CreateSettlementRequest) (*Settlement, error)
// Bad: missing context, unclear naming
func (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 domain package (e.g. ErrInvitationExpired) and map them once, in the use case layer, to pkg/apperror errors carrying a gRPC code and a stable reason (e.g. INVITATION_EXPIRED) plus field details for validation. The reason becomes the API error.code the 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)
}
// domain
var ErrInvitationExpired = errors.New("invitation expired")
// usecase: one mapping table per module
case errors.Is(err, domain.ErrInvitationExpired):
return apperror.Wrap(apperror.CodeFailedPrecondition, err.Error(), err).WithReason(domain.ReasonInvitationExpired)
// validation with a field detail
return 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-unit fails below 90% on internal/... and pkg/... (generated mocks and *test helper 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 scripted dbtest pgx fake and covered end-to-end by integration tests (build tag integration, testcontainers) that run as the vietgara_app role so RLS applies.
  • Tests live in _test packages (the testpackage linter) and call t.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.http rule (API Specification, Section 1.1): resource in the body for Create/Update (body: "garage"), update_mask for partial updates, repeated … data + Pagination for lists, field_behavior on 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 Policies table (public, unverified, user, admin or garage rule); a missing policy is rejected at runtime.

2.7. Required Tooling in CI

ToolPurpose
gofmt / gofumptCode 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-integrationIntegration 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 helpers

3.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.ts only aliases them. CI fails when schema.d.ts is stale.
  • All requests go through lib/api.ts, which adds the bearer token, refreshes it once on 401, and throws ApiError with the API code and field details.

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 queryKey factory per resource (e.g. ['garages', garageId, 'customers']), invalidateQueries after mutations, keepPreviousData for 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 a label on every cell so it stacks on phones), Badge, PageLayout + PageHeader, Panel. Do not make every section a card; use Section for titled groups without a box.

  • Every data screen implements its states: QueryState (skeleton / error with retry), EmptyState with the next action, no-results with Clear filters, and a toast after a successful action.

  • Controls get accessible names (icon-only buttons use IconButton with 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; honor prefers-reduced-motion (the kit’s UIProvider does).

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 translates errors.<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-ui package (repo vietgara-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’s src/app/styles.css only imports Tailwind, the kit’s stylesheet and an @source for 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), tag vX.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.ts excludes it from pre-bundling (optimizeDeps.exclude). The lockfile must record the git+https URL; Docker builds install git.
  • The shell’s collapsible sidebar and its remembered state come from the kit (AppShell); an app only supplies the sidebar contents and the collapse/expand labels, and uses useSidebarCompact() for anything a collapsed rail hides.
  • Themes: UIProvider (theme, tooltips, reduced motion, toasts) wraps the app; index.html carries the no-flash theme script.

3.7. Testing & Linting

ToolPurpose
ESLint (typescript-eslint, react-hooks)Lint TypeScript/React
Prettier (.prettierrc.json)Automatic code formatting (npm run format:check in CI)
tsc -bType checking
Vitest + React Testing Library + jsdomTests 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 buildvietgara-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 selectorsPrefer 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 injection

4.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 through context.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.0

A 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.

RepositoryIdentifier
vietgara-owner-appcom.viettechno.vietgara.owner
vietgara-admin-appcom.viettechno.vietgara.admin
vietgara-customer-appcom.viettechno.vietgara.customer
vietgara-technician-appcom.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) from vietgara_core (v0.4.0+) for theme and darkTheme and follows the system theme. Colors, radii, type and component themes come from there; read status and plate colors with context.vg (VgColors), radii from VgRadius. No Color(0x…) or Colors.* in an app for UI semantics.
  • Use the core widgets for repeating patterns: StatusChip (icon + tone), PlateChip for 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

ToolPurpose
dart analyze + package lints (effective_dart)Standard Dart linting
flutter test (widget tests)Behavior-focused UI testing, no real device required
mocktailMock 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).

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.