cd /news/developer-tools/nicks-rules-for-coding-my-personal-r… · home › topics › developer-tools › article
[ARTICLE · art-112478] src=gist.github.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

/nicks-rules-for-coding - my personal rules for better agentic coding output

Nick Basile has published version 2.2.0 of his personal coding rules, a set of guidelines for architecture, Laravel, testing, databases, APIs, Inertia.js, React, Tailwind, HTML/JSX, and accessibility. The rules emphasize server-side data transformation, avoiding hidden form fields, and enforcing conventions through linters.

read10 min views24 publishedAug 25, 2026

| name | nicks-rules-for-coding | | version | 2.2.0 | | description | Nick's personal coding rules for architecture, Laravel, testing, databases, APIs, Inertia.js, React, Tailwind, HTML/JSX, and accessibility. Use when writing or reviewing code in any of these stacks to match Nick's preferences. | | metadata | | author | tags | Nick Basile | architecture, laravel, inertia, react, tailwind, conventions, accessibility, testing, database, api | |

Apply these rules whenever you write or review code in Nick's projects that use any of these stacks.

Scope. Apply every rule to code you are writing or modifying. For violations in code you are not touching, flag them briefly and move on. Don't fix them unless asked. Unrequested refactors expand the diff and the risk (see Architecture rule 1). Precedence. When these rules conflict with php-guidelines-from-spatie

, react-conventions

, or any other general guideline, these rules win. The sibling skills fill the gaps this one doesn't cover. Versions. The Tailwind rules assume v4 and the Inertia rules assume v2. Verify the installed version before applying version-specific syntax. On older versions, apply the closest equivalent and mention the mismatch.

Linting. Rules tagged [lint: ...] can be enforced mechanically. If the project's linter doesn't already enforce a tagged rule, suggest wiring it up (see Architecture rule 7). Stack match. Not every project uses every stack here. Apply only the sections that match what's actually installed and ignore the rest, e.g. on a Next.js project skip the Laravel, Inertia, and Testing sections but still apply Architecture, Comments, React, Tailwind, HTML/JSX, and Accessibility. The stack-agnostic sections always apply.

  • Don't prematurely optimize for features or patterns that are not actively being worked on. Only optimize for the feature at hand in the context of the existing codebase.
  • As much as possible, data should be transformed and prepped on the server instead of the client. The server has the full picture and one implementation; client-side prep duplicates logic and ships more JavaScript.
  • Before adding a dependency, check whether one already installed covers the need. Prefer the lighter option, e.g. a REST client over one that drags in native extensions or a large dependency tree, even if both work.
  • Push filtering, sorting, and limiting down to the data source (the DB query or the API request). Don't fetch everything and slice it in app code. The database is built for this, and it keeps app memory flat.
  • Hidden form fields are a code smell, a sign the logic hasn't been architected correctly. They usually smuggle server state through the client, where it can drift or be tampered with. Always first examine whether the logic can move to the backend. Sometimes a hidden field is necessary, but start with that review every time.
  • Always kill any dead or unused code unless there's an explicit comment to keep it.

[lint: PHPStan dead-code rules, ESLint no-unused-vars]

  • Always try to codify new rules and guards in our linters as we discover them. When a new convention or constraint emerges, encode it as a lint rule so it's enforced automatically rather than relying on memory or review.
  • Never weaken a linter to get it to pass. Fix the code, not the rule. Don't disable, downgrade, or add ignore comments to silence a linter just to make it green.
  • Stay synchronous until it hurts. Queued jobs, events, and listeners are indirection; add them when something is measurably slow, not by default.
  • Always strive to use the least amount of code possible to solve any problem. Keep it simple, stupid.
  • Readable code is good code. We should be able to easily understand what's happening. Don't get cute or fancy; it's never worth it.

Comments #

  • Use comments sparingly.

  • Never use comments to explain what the code obviously tells us, and never use them to track progress.

  • Good comments link to documentation or capture non-obvious decisions and trade-offs.

  • Only use RESTful verbs for method names in controllers ( index

, create

, store

, show

, edit

, update

, destroy

). If you feel the need to add another verb, make a new controller instead. [lint: Pest arch test]

  • For reusable functionality, use actions or extend a base controller. Never use traits in controllers.

[lint: Pest arch test]

  • Prefer actions over service classes, and only create an action once code is actually reused (the second use). Until then, single-use business logic lives inline in the controller method. Premature extraction buys indirection without reuse.
  • When defining routes, use the resource methods like
`Route::resource()`

or `Route::apiResource()`

.

  • Invokable, single-action controllers are great.

  • Build queries inline in the controller or action until the logic is reused, then extract to model scopes. Eager-load relationships explicitly and treat N+1 queries as bugs. [guard: Model::preventLazy() in non-production — note Laravel 12.8+ auto-eager-loads collection relations before the check fires, so the guard is a backstop; explicit query-builder calls in loops (e.g. ->where()->first() per row) are still N+1s it cannot catch]

  • Middleware should remain focused on user permissions, authentication, billing, and state management. When other logic shows up there, move it by kind: authorization to policies or gates, input validation to form requests, mutations and side effects to the controller or an action.

  • Validation and authorization are graduated. Inline

$request->validate() is fine for a couple of fields; extract a FormRequest as rules grow or get reused. Manual authorization checks are fine early; reach for a Policy once a model has real ownership rules.

  • Throw framework exceptions ( ValidationException

, ModelNotFoundException

, abort() ) and let Laravel's handler render them. Rarely create custom exception classes; only when a domain failure genuinely needs its own handling.

  • Always check the Laravel docs to see if there's a Laravel-native way to implement this before writing custom code. Follow framework conventions as closely as possible.

  • When integrating with a third-party API, always create a dedicated Gateway class in app/Gateways/

to house the integration.

  • Name the class after the service ( StripeGateway

, PokedexGateway

). Name the methods for intent (chargeCard

, fetchPokemon

), not for endpoints.

  • Return the raw response and let the caller handle errors and extract data. The right error handling depends on the call site's context, so the Gateway shouldn't decide it.
  • Test Gateway consumers with

Http::fake() as much as possible.

  • Use Pest with Laravel's testing helpers. Use datasets instead of repeating near-identical tests.

  • Feature-first: most tests hit the route and assert the response and resulting database state. Write unit tests only for gnarly isolated logic (calculations, parsers).

  • Don't test framework behavior. Test your rules, not Laravel's.

  • Follow Laravel's naming and migration conventions throughout.

  • Always define foreign key constraints.

  • Prefer nullable timestamps over booleans for state ( published_at

over is_published

). A timestamp records when it happened, and null

still reads as false.

  • Prefer generated columns and database defaults over app-level defaults where sensible.

  • Before a project or feature has deployed, rewrite the original migration instead of stacking fix-up migrations. Once deployed, migrations are append-only.

  • Always store monetary values as integer cents. Float math drops pennies, and decimals invite rounding drift the moment app code touches them; integer math is exact.

  • Default numeric columns to integers generally. There's rarely a compelling case for floats or decimals; store the value in its smallest whole unit (cents, grams, basis points) and convert at the edge for display.

  • Use

`Route::apiResource()`

for routes, API Resources for responses, and Sanctum for auth.
- Version from day one:

/api/v1

from the very first route.

  • Stick with the envelope API Resources produce ( data

, plus meta

/links

for pagination). Don't hand-roll a custom envelope.

  • Error shape and auth details beyond Sanctum vary; follow the project's existing convention rather than a default.

  • Lean into Inertia conventions as much as possible. Never do something in raw Vue.js or React.js that you can accomplish with Inertia directly.

  • Use the Inertia Form

component.

  • Rely on the server for state. Avoid tracking it in client components. Two sources of truth drift; the server already knows.
  • Reach for Inertia v2 features before JavaScript workarounds: deferred props for slow data, usePoll

instead of a hand-rolled setInterval

, WhenVisible

for below-the-fold data. Hand-rolling one of these in React violates rule 1.

  • Skip link prefetching. It's usually more trouble than it's worth.
  • Shape page props on a ladder: start with inline arrays shaped in the controller. Move to API Resources once the shape is reused or the project has an API. Reach for spatie/laravel-data

DTOs when data consistency is essential and the shape is complicated.

  • If a controller is sending more than 5 properties to an Inertia page, that's a good sign to pass a resource or the whole object instead of a bunch of denormalized properties. If you're transforming a lot of properties, use a resource.

  • Avoid useEffect

as much as possible. Work down this ladder before reaching for it:

  • Derive values during render instead of syncing them into state ( useMemo

if the calculation is expensive).

  • Put logic in event handlers; the POST belongs in the click handler, not an effect watching state.
  • Reset component state with a key

prop instead of clearing it in an effect.

  • Let the server own the state through Inertia (see Inertia rules 3 and 4). useEffect

is for synchronizing with genuinely external systems: subscriptions, DOM APIs, third-party widgets. See You Might Not Need an Effect.

  • Multiple components in one file are fine if they're not being reused elsewhere.

  • TypeScript is there for autocomplete and catching obvious mistakes, not ceremony. Inline prop types are fine. Don't chase end-to-end type generation or fight the compiler.

  • Stick to the default spacing values as much as possible.

  • For repeated or design-system values (colors, fonts, shared sizes), add them to the theme instead of using arbitrary values. Arbitrary values are fine for one-off layout sizing (e.g.,

`max-w-[480px]`

, `min-w-[240px]`

) where adding a theme token would be overkill.

  • Extract repeated utility chains into @layer components

classes: typography (.h1 /.h2

/.h3

, .p

/.p-sm

/.p-lg

), buttons (.btn , .btn-primary

), links (.nav-link ), and any other element repeated across the page. Don't repeat the same long utility chain in HTML.

  • Define design tokens (colors, fonts) via @theme

CSS variables in your stylesheet, not arbitrary hex values in HTML.

  • Inside @layer components

classes, use the @variant

directive for hover/focus/etc. states (e.g., @variant focus, hover { @apply ... }

) instead of writing separate selectors or duplicating utility chains.

  • Write the least amount of HTML necessary to accomplish the goal.
  • Always set explicit width

and height

attributes on <img>

tags to prevent layout shift.

  • Add ="lazy"

to below-the-fold images. Leave hero/first-paint images eager (no attribute).

  • Use alt=""

for purely decorative images. Use descriptive alt text for content images. [lint: jsx-a11y/alt-text]

  • Prefix JavaScript hooks with js-
(e.g., `id="js-lightbulb"`

, `class="js-testimonial"`

) to separate JS selectors from styling classes.

  • Use data-*

attributes for state, paired with Tailwind's data-*:

variant for styling (e.g., toggle data-active

from JS, style with group-data-active:-translate-y-6 ).

  • External links always pair target="_blank"

with rel="noopener noreferrer"

. [lint: react/jsx-no-target-blank]

  • Use semantic elements. Interactive things are <button>

or <a>

, never a <div>

with an onClick

. [lint: jsx-a11y/no-static-element-interactions]

  • Every input gets a label.

[lint: jsx-a11y/label-has-associated-control]

  • Keep visible focus states. Never remove an outline without providing a replacement.
  • Respect

prefers-reduced-motion on any custom keyframe animation. Disable or reduce the animation inside an @media (prefers-reduced-motion: reduce)

block.

── more in #developer-tools 4 stories · sorted by recency
── more on @nick basile 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/nicks-rules-for-codi…] indexed:0 read:10min 2026-08-25 · —