A coding standards guide for AI-assisted development on this repo. Read this before generating or reviewing code.
Full-stack monorepo:
- Backend: Go 1.26.1 HTTP API (
/server) - Frontend: React 19 + TypeScript 5.9 + Tailwind CSS 4 + Vite 7 (
/web) - Module:
github.com/String-sg/teacher-workspace - Package manager: pnpm
/server
/cmd/tw – Go binary entrypoint (main.go)
/internal/ – handler, middleware, config (private)
/pkg/ – dotenv, require (reusable/public)
/web
/components/ – reusable React components (PascalCase dir + index.ts barrel)
/containers/ – page/view-level containers
/hooks/ – custom React hooks
/helpers/ – utility functions
/.github/workflows – ci.yaml (lint/format), sast.yml (CodeQL)
| Thing | Convention | Example |
|---|---|---|
| Packages | lowercase no underscores |
handler, dotenv |
| Exported types/funcs | PascalCase |
Handler, RequestOTP |
| Unexported | camelCase |
cfg, sessionID |
| Constants | UPPER_SNAKE_CASE |
ErrorCodeInvalidForm |
| Test functions | Test<Func>_<Scenario> |
TestRequestOTP_SuccessProduction |
| Env vars | TW_ prefix, UPPER_SNAKE_CASE |
TW_SERVER_PORT |
| Struct tags | dotenv:"TW_*", json:"field_name" |
| Thing | Convention | Example |
|---|---|---|
| Components & component files | PascalCase |
AppCard.tsx |
| Hooks | use prefix, camelCase |
useIsMobile.ts |
| Utility files | camelCase |
dateTime.ts |
| Constants | UPPER_SNAKE_CASE |
MOBILE_BREAKPOINT |
| Context types | suffix Context |
SidebarContext |
| Boolean vars | is, has, can prefix |
isMobile, isOpen |
| Path alias | ~/ → ./web/ |
import { X } from '~/components/X' |
- Formatted by
gofmt+goimports— never hand-format imports - Use
anyinstead ofinterface{}(golangci auto-rewrites this) - Exported docstrings start with the symbol name:
// Register attaches all application routes to the provided ServeMux. func (h *Handler) Register(mux *http.ServeMux) { … }
- Prettier config:
printWidth: 100,tabWidth: 2,singleQuote: true,trailingComma: 'all' - Tailwind classes auto-ordered by
prettier-plugin-tailwindcss - Arrow function components with explicit props type:
const MyComponent: React.FC<MyComponentProps> = ({ foo }) => { … };
- JSDoc on all exported functions/hooks:
/** * A hook to check if the viewport is mobile. * * @returns `true` if the viewport is mobile, `false` otherwise. */ export function useIsMobile(): boolean { … }
Groups separated by a blank line: stdlib → external → internal
import (
"context"
"net/http"
"golang.org/x/sync/errgroup"
"github.com/String-sg/teacher-workspace/server/internal/config"
"github.com/String-sg/teacher-workspace/server/internal/handler"
)Ordered automatically by eslint-plugin-simple-import-sort:
- Side-effect imports (CSS):
import './App.css' - External packages:
import React from 'react' - Internal (
~/or relative):import { Sidebar } from '~/components/Sidebar'
- Always return
(T, error)— never swallow errors - Wrap with context:
fmt.Errorf("requestOTP: %w", err) - Join multiple validation errors:
errors.Join(errs...) - HTTP errors via handler helpers:
writeClientErrorResponse(w, http.StatusBadRequest, ErrorCodeInvalidForm) writeServerErrorResponse(w, logger, err)
- Named error code constants:
ErrorCodeInvalidForm,ErrorCodeInvalidAuth - Detect timeouts:
errors.Is(err, context.DeadlineExceeded) - Deferred cleanup with error capture:
defer func() { if err := r.Body.Close(); err != nil { … } }()
- Test files:
*_test.gocollocated with source file - Top-level naming:
Test<FunctionName>_<Scenario> - Subtests:
t.Run("scenario description", func(t *testing.T) { … }) - Assertions: use helpers from
server/pkg/require— nottesting.TdirectlyAll helpers callrequire.Equal(t, expected, actual) require.NoError(t, err) require.True(t, condition)
t.Helper()internally. - HTTP mocking:
RoundTripperFunctype (defined in handler test files) - State reset: use cleanup helpers (e.g.
resetStore()) before each test case
TypeScript tests: not yet added — follow Go's colocation pattern when introduced.
| Command | What it does |
|---|---|
pnpm dev |
Vite dev server |
pnpm build |
TS compile + Vite build → dist/ |
pnpm format |
Prettier check (all file types) |
pnpm lint |
ESLint on JS/TS |
pnpm golint |
golangci-lint on Go |
pnpm prepare |
Setup Husky hooks |
All checks run via GitHub Actions on every PR:
| Workflow | Jobs |
|---|---|
ci.yaml |
Prettier format → ESLint → golangci-lint |
sast.yml |
CodeQL for JS/TS + Go (also runs on push to main) |
Pre-commit (Husky + lint-staged): Prettier + ESLint fix run automatically on staged files.
All CI checks must pass before merging.
- Secrets are never committed — use
.env.*files (gitignored) - Copy
.env.exampleto.envlocally; never commit.env - Config is loaded via
server/pkg/dotenvand mapped to structs withdotenv:"TW_*"tags - All env var names are prefixed
TW_