This repository documents the conventions I follow on personal and professional projects, and ships the @carlos3g/* config packages that encode them. Meant as a quick reference for myself, teammates, and agents (Claude Code, Cursor, etc).
The conventions below are published as composable @carlos3g/* config packages
— install them instead of copy-pasting config between projects.
| Package | What it gives you |
|---|---|
@carlos3g/eslint-config |
ESLint flat-config presets — base, nest, react, expo, jest, prettier. |
@carlos3g/prettier-config |
The shared Prettier config. |
@carlos3g/tsconfig |
tsconfig bases — node, nestjs, next, vite-react, react-native. |
@carlos3g/commitlint-config |
The shared commitlint (Conventional Commits) config. |
@carlos3g/create-config |
Scaffolds a new project onto all of the above in one command. |
The fastest way to apply this style to a project is the scaffolder — it writes
every config file and wires package.json:
npm create @carlos3g/configOr wire a single tool by hand, e.g. ESLint:
// eslint.config.mjs — NestJS API
import nest from '@carlos3g/eslint-config/nest';
export default nest;Each package's README has full usage. The presets, configs and conventions are versioned together in this monorepo.
- Principle of least astonishment
- Bulletproof React as the baseline organization for frontends
- Conventional Commits
- Airbnb JavaScript Style Guide as the starting point
- Language: TypeScript (
strict: true,noImplicitAny,strictNullChecks) - Runtime: Node.js LTS (
lts/jodvia.nvmrc) - Package manager: Yarn 4 (Berry) with
nodeLinker: node-modules - Monorepo: Turborepo + workspaces (
apps/*,packages/*) - Backend: NestJS + Prisma + PostgreSQL
- Mobile: Expo + React Native + NativeWind
- Server state: TanStack Query
- Client state: Zustand
- Local persistence (mobile): react-native-mmkv
- Forms: react-hook-form + zod
- ESLint:
@carlos3g/eslint-config(flat config,typescript-eslint/strictTypeChecked) - Prettier:
@carlos3g/prettier-config— single quotes,es5trailing commas, 120 print width, 2-space tabs - TypeScript:
@carlos3g/tsconfig— stricttsconfigbases per stack - EditorConfig: LF, UTF-8, 2 spaces, final newline, no trailing whitespace
- Commits: commitlint via
@carlos3g/commitlint-config - Pre-commit: lint-staged —
prettier --write+eslint --fixon JS/TS;prettier --writeon JSON/MD/YAML - Git hooks: husky
commit-msg: commitlintpre-commit: lint-stagedpre-push:style(format + lint + typecheck) +test+test:e2e
- Files:
kebab-case.ts. Suffixes by role:*.controller.ts,*.module.ts,*.service.ts,*.entity.ts*.use-case.ts— one use case per file*.contract.ts— interface/abstract class contract*.repository.ts— implementation (e.g.prisma-quote.repository.ts)*.e2e-spec.tsnext to the controller/repository under test
- Classes:
PascalCase - Variables and functions:
camelCase - Constants:
UPPER_CASEallowed - React components:
PascalCase, filekebab-case.tsx, arrow function + named export - Hooks:
use-*.ts, exporting auseXfunction - DTOs (Spring Boot-style suffixes — same convention I apply on Java projects, reference):
*-request.ts— incoming HTTP payload (validated withclass-validator)*-query.ts— HTTP query params*-input.ts— use case input (already enriched with user/context)*-repository-dtos.ts— types consumed by repositories
The patterns below are compiled, linted code in
examples/nest-api — not just snippets.
<domain>/
├── contracts/ # abstract classes (repository/service interfaces)
├── dtos/ # request, query, input, repository dtos
├── entities/ # domain entities
├── repositories/ # Prisma implementations of the contracts
├── services/ # domain services when justified
├── use-cases/ # one use case per feature
├── <domain>.controller.ts
└── <domain>.module.ts
- Repository pattern: every database access goes through an
abstract class *RepositoryContract. The module wires the concrete implementation:providers: [{ provide: QuoteRepositoryContract, useClass: PrismaQuoteRepository }];
- One use case per feature: each use case is an
@Injectableclass that implementsUseCaseHandlerand exposes a singlehandle(input)method. No giant*Servicegod-classes. - Thin controllers: only receive the request, normalize it, and delegate to the use case.
- Explicit
publicon every member (@typescript-eslint/explicit-member-accessibility). type-only imports whenever possible (@typescript-eslint/consistent-type-imports).- Validate at the edge:
class-validatoron*Request/*Query, globalValidationPipe. - URI versioning:
@Controller({ path: 'quotes', version: '1' }). - Path aliases:
@app/*forsrc/,@test/*fortest/. - REST: controller methods follow
index/show/store/update/destroywhen applicable; non-CRUD actions are named after the verb (favorite,share).
@Injectable()
export class FavoriteQuoteUseCase implements UseCaseHandler {
public constructor(private readonly quoteRepository: QuoteRepositoryContract) {}
public async handle(input: FavoriteQuoteInput): Promise<void> {
const { quoteUuid, user } = input;
const quote = await this.quoteRepository.findUniqueOrThrow({ where: { uuid: quoteUuid } });
if (await this.quoteRepository.isFavorited({ where: { quoteId: quote.id, userId: user.id } })) {
return;
}
await this.quoteRepository.favorite({ data: { quoteId: quote.id, userId: user.id } });
}
}- Prisma as the ORM, schema in
prisma/schema.prisma prisma/seeders/andprisma/factories/for fixtures- Internal IDs:
id(int autoincrement); public IDs:uuid— APIs always expose uuid, neverid
- Unit: Jest + mocks of the contracts
- E2E:
*.e2e-spec.tsnext to the controller/repository, real database (Postgres in Docker), no DB mocks test/at the app root holds helpers, factories, and the test server bootstrap
Frontend (Bulletproof React)
src/
├── features/<feature>/
│ ├── components/
│ ├── contracts/ # service interfaces
│ ├── hooks/ # use-* (React Query + logic)
│ ├── services/ # class implementing the contract + singleton instance
│ ├── utils/
│ └── (contexts|store|enums|validations) when needed
├── shared/ # cross-feature components, hooks, services, utils, theme
├── lib/<library>/ # config for external libs (axios, react-query, i18n, zod...)
├── app/ # routes (Expo Router) or screens/
├── navigation/
└── types/ # global types (api, http, entities)
- Path alias:
@/*→src/* - Service per feature: a class implementing a
*ServiceContract, instantiated once:const quoteService: QuoteServiceContract = new QuoteService(httpClientService);
- HTTP centralized in
shared/services/http-client-service(axios) — features never import axios directly. - React Query for any network call;
queryKeyscentralized inlib/react-query/query-keys.ts. - Optimistic mutations with
onMutate+ rollback inonErrorwhen UX demands instant feedback. - Zustand for UI/session state; MMKV for persistence.
- Components are always
arrow functions — noReact.FC. - i18n via
react-i18next— no hardcoded strings in UI. - Toasts standardized (
sonner-native) for mutation errors.
- Conventional Commits:
feat,fix,chore,refactor,test,docs,style,perf,ci,build - Messages in English, imperative, lowercase (
feat: add quote sharing) - 1 PR = 1 reason. Refactors don't ride along with features.
mainis always deployable; work happens onfeat/...,fix/...branches.
- Fix the root cause, not the symptom. Bypasses (
--no-verify,eslint-disable,as any) are debt — record the why. - Don't introduce abstraction before the third repetition.
- Don't comment the what — names handle that. Only comment the why when it isn't obvious.
- Validate at the edges (HTTP, storage, external libs). Trust the internal code.
- Delete completely instead of leaving
// removednotes or_unusedshims.
- Agent / contributor guide: AGENTS.md
- Config packages:
@carlos3g/*(this repo) - Conventional Commits: conventionalcommits.org
- Bulletproof React: github.com/alan2207/bulletproof-react
- Airbnb JS: github.com/airbnb/javascript
- DTO naming (Spring Boot convention, also applied to TS/Java projects): stackoverflow.com/a/35341664
- lint-staged: github.com/lint-staged/lint-staged
- commitlint: commitlint.js.org