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