Skip to content

carlos3g/code-style

Repository files navigation

My code style

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

Config packages

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/config

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

Prerequisites

Base stack

Tooling

  • ESLint: @carlos3g/eslint-config (flat config, typescript-eslint/strictTypeChecked)
  • Prettier: @carlos3g/prettier-config — single quotes, es5 trailing commas, 120 print width, 2-space tabs
  • TypeScript: @carlos3g/tsconfig — strict tsconfig bases per stack
  • EditorConfig: LF, UTF-8, 2 spaces, final newline, no trailing whitespace
  • Commits: commitlint via @carlos3g/commitlint-config
  • Pre-commit: lint-stagedprettier --write + eslint --fix on JS/TS; prettier --write on JSON/MD/YAML
  • Git hooks: husky
    • commit-msg: commitlint
    • pre-commit: lint-staged
    • pre-push: style (format + lint + typecheck) + test + test:e2e

Naming conventions

  • 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.ts next to the controller/repository under test
  • Classes: PascalCase
  • Variables and functions: camelCase
  • Constants: UPPER_CASE allowed
  • React components: PascalCase, file kebab-case.tsx, arrow function + named export
  • Hooks: use-*.ts, exporting a useX function
  • DTOs (Spring Boot-style suffixes — same convention I apply on Java projects, reference):
    • *-request.ts — incoming HTTP payload (validated with class-validator)
    • *-query.ts — HTTP query params
    • *-input.ts — use case input (already enriched with user/context)
    • *-repository-dtos.ts — types consumed by repositories

Backend (NestJS)

The patterns below are compiled, linted code in examples/nest-api — not just snippets.

Module layout

<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

Patterns

  • 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 @Injectable class that implements UseCaseHandler and exposes a single handle(input) method. No giant *Service god-classes.
  • Thin controllers: only receive the request, normalize it, and delegate to the use case.
  • Explicit public on every member (@typescript-eslint/explicit-member-accessibility).
  • type-only imports whenever possible (@typescript-eslint/consistent-type-imports).
  • Validate at the edge: class-validator on *Request / *Query, global ValidationPipe.
  • URI versioning: @Controller({ path: 'quotes', version: '1' }).
  • Path aliases: @app/* for src/, @test/* for test/.
  • REST: controller methods follow index / show / store / update / destroy when applicable; non-CRUD actions are named after the verb (favorite, share).

Use case example

@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 } });
  }
}

Database

  • Prisma as the ORM, schema in prisma/schema.prisma
  • prisma/seeders/ and prisma/factories/ for fixtures
  • Internal IDs: id (int autoincrement); public IDs: uuid — APIs always expose uuid, never id

Tests

  • Unit: Jest + mocks of the contracts
  • E2E: *.e2e-spec.ts next 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

Layout

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)

Patterns

  • 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; queryKeys centralized in lib/react-query/query-keys.ts.
  • Optimistic mutations with onMutate + rollback in onError when UX demands instant feedback.
  • Zustand for UI/session state; MMKV for persistence.
  • Components are always arrow functions — no React.FC.
  • i18n via react-i18next — no hardcoded strings in UI.
  • Toasts standardized (sonner-native) for mutation errors.

Git & commits

  • 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.
  • main is always deployable; work happens on feat/..., fix/... branches.

Principles

  • 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 // removed notes or _unused shims.

Links

About

My code-style conventions and @carlos3g/eslint-config — flat-config ESLint presets for TypeScript, NestJS and Expo/React Native

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors