Best practices for every stack
108 curated best practices across 8 languages, including 42 security rules mapped to OWASP Top 10 and CWE IDs. The BeforeMerge AI review engine references these practices to deliver specific, authoritative code review feedback.
Categories
Each practice is classified into one of these categories. The AI review engine selects relevant practices based on your repository's detected languages and frameworks.
Security
42 practicesPrevent vulnerabilities and protect user data
Performance
23 practicesOptimize speed, memory, and resource usage
Accessibility
3 practicesEnsure inclusive and usable interfaces
Architecture
16 practicesMaintainable structure and design patterns
Testing
7 practicesReliable tests that catch real bugs
Error Handling
8 practicesGraceful failure and recovery
Code Quality
9 practicesReadable, maintainable, and idiomatic code
Best practices by language
Browse all practices organized by language and framework. Each practice includes a description, code examples, and references.
TypeScript (34 practices)
Never use eval() or Function() constructor
eval() and new Function() execute arbitrary strings as code, enabling code injection attacks. Use structured alternatives like JSON.parse() or a sandboxed interpreter.
const result = eval(userInput);const data = JSON.parse(userInput);Sanitize dangerouslySetInnerHTML input
Rendering unsanitized HTML via dangerouslySetInnerHTML creates XSS vulnerabilities. Always sanitize with a library like DOMPurify before rendering.
<div dangerouslySetInnerHTML={{ __html: userContent }} />import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userContent) }} />Validate all user input at system boundaries
Parse and validate user input with a schema library (Zod, Yup, io-ts) at API routes, server actions, and form handlers. Never trust client-side validation alone.
export async function updateProfile(formData: FormData) {
const name = formData.get("name") as string;
await db.update({ name });
}const schema = z.object({ name: z.string().min(1).max(100) });
export async function updateProfile(formData: FormData) {
const { name } = schema.parse(Object.fromEntries(formData));
await db.update({ name });
}Use parameterized queries, never string interpolation
Building SQL queries with template literals or string concatenation enables SQL injection. Use parameterized queries or an ORM query builder.
const result = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);const result = await db.query("SELECT * FROM users WHERE id = $1", [userId]);Never expose secrets in client-side code
API keys, database credentials, and service tokens must only be used server-side. Use NEXT_PUBLIC_ prefix only for truly public values. Never import server modules in client components.
// In a client component
const supabase = createClient(process.env.SUPABASE_SERVICE_ROLE_KEY!);// In a server action
const supabase = createAdminClient(); // uses server-only env var internallyUse Server Actions or CSRF tokens for mutations
Next.js Server Actions include built-in CSRF protection. If using API routes for mutations, implement CSRF token validation to prevent cross-site request forgery.
Validate file paths to prevent directory traversal
User-supplied file paths can escape intended directories with ../ sequences. Always resolve and validate paths against a base directory.
const filePath = path.join(uploadDir, req.query.filename);
fs.readFileSync(filePath);const filePath = path.resolve(uploadDir, req.query.filename);
if (!filePath.startsWith(path.resolve(uploadDir))) {
throw new Error("Invalid path");
}
fs.readFileSync(filePath);Guard against prototype pollution
Deep merge or Object.assign with user-controlled keys can pollute Object.prototype. Use Object.create(null) for dictionaries and validate keys against __proto__, constructor, prototype.
function merge(target: any, source: any) {
for (const key in source) {
target[key] = source[key]; // allows __proto__ pollution
}
}function merge(target: Record<string, unknown>, source: Record<string, unknown>) {
const forbidden = new Set(["__proto__", "constructor", "prototype"]);
for (const key of Object.keys(source)) {
if (forbidden.has(key)) continue;
target[key] = source[key];
}
}Avoid catastrophic backtracking in regular expressions
Complex regex with nested quantifiers can cause ReDoS (Regular Expression Denial of Service). Use atomic groups, possessive quantifiers, or test with safe-regex.
const pattern = /^(a+)+$/; // exponential backtrackingconst pattern = /^a+$/; // simple, no nested quantifiersConfigure Content Security Policy headers
Set CSP headers to prevent XSS, clickjacking, and other injection attacks. Use next.config.js headers or middleware to enforce CSP across your application.
Avoid N+1 query patterns
Fetching related data in a loop creates N+1 queries. Use joins, batch fetches, or .in() filters to load related data in a single query.
const users = await db.from("users").select("id");
for (const u of users) {
const posts = await db.from("posts").select("*").eq("user_id", u.id);
}const users = await db.from("users").select("id, posts(*)");
// Or batch: const posts = await db.from("posts").in("user_id", userIds);Use React.memo and useMemo appropriately
Wrap expensive pure components with React.memo and memoize costly computations with useMemo. But avoid premature memoization — measure first.
function ExpensiveList({ items }: { items: Item[] }) {
const sorted = items.sort((a, b) => a.name.localeCompare(b.name));
return <>{sorted.map(item => <ListItem key={item.id} item={item} />)}</>;
}const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
return <>{sorted.map(item => <ListItem key={item.id} item={item} />)}</>;
});Avoid blocking the event loop
Synchronous operations (fs.readFileSync, crypto.pbkdf2Sync, large JSON.parse) block the event loop. Use async variants or offload to a worker thread.
const data = fs.readFileSync("/large-file.json", "utf8");
const parsed = JSON.parse(data);const data = await fs.promises.readFile("/large-file.json", "utf8");
const parsed = JSON.parse(data);Use dynamic imports for large dependencies
Importing heavy libraries at the top level increases bundle size and initial load time. Use dynamic import() for libraries only needed in specific code paths.
import { marked } from "marked"; // always in bundle
export function renderMarkdown(md: string) {
return marked(md);
}export async function renderMarkdown(md: string) {
const { marked } = await import("marked");
return marked(md);
}Always LIMIT database queries
Fetching all rows from a table without a LIMIT can return millions of rows and crash the application. Always paginate or set a reasonable limit.
const { data } = await supabase.from("logs").select("*");const { data } = await supabase.from("logs").select("*").order("created_at", { ascending: false }).limit(100);Use useCallback for handlers passed as props
Inline arrow functions in JSX create new references on every render, breaking React.memo on child components. Wrap handlers with useCallback when passed to memoized children.
function Parent() {
return <MemoChild onClick={() => doSomething()} />;
}function Parent() {
const handleClick = useCallback(() => doSomething(), []);
return <MemoChild onClick={handleClick} />;
}Use next/image for automatic image optimization
The next/image component automatically serves optimized, responsive images with lazy loading. Using raw <img> tags misses these optimizations.
<img src="/hero.png" alt="Hero" />import Image from "next/image";
<Image src="/hero.png" alt="Hero" width={800} height={400} />Parallelize independent async operations
Sequential awaits for independent operations waste time. Use Promise.all() for independent operations, or Promise.allSettled() when individual failures should not abort the batch.
const users = await fetchUsers();
const posts = await fetchPosts();
const comments = await fetchComments();const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments(),
]);Avoid setState in useEffect — derive state instead
Setting state inside useEffect to transform props causes unnecessary re-renders and sync bugs. Derive the value during render or use useMemo.
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);const fullName = `${first} ${last}`;Use Error Boundaries for component failure isolation
Without Error Boundaries, a single component crash takes down the entire tree. Wrap feature sections in Error Boundaries to contain failures gracefully.
// No error boundary — crash propagates to root
<App>
<Dashboard />
<Analytics /> {/* crash here kills everything */}
</App><App>
<Dashboard />
<ErrorBoundary fallback={<p>Analytics unavailable</p>}>
<Analytics />
</ErrorBoundary>
</App>Use stable, unique keys for list rendering
Using array index as key causes incorrect reconciliation when items are reordered, added, or removed. Use a stable unique identifier from the data.
{items.map((item, index) => <ListItem key={index} item={item} />)}{items.map(item => <ListItem key={item.id} item={item} />)}Clean up side effects in useEffect
Event listeners, intervals, subscriptions, and AbortControllers must be cleaned up in the useEffect return function to prevent memory leaks.
useEffect(() => {
window.addEventListener("resize", handler);
}, []);useEffect(() => {
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);Prefer controlled components or form libraries for forms
Uncontrolled inputs with ref.current.value are error-prone and hard to validate. Use controlled components with state, or a form library like react-hook-form.
const ref = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
const value = ref.current?.value;
};const [value, setValue] = useState("");
<input value={value} onChange={e => setValue(e.target.value)} />Use Suspense for async data loading patterns
Suspense provides a declarative loading pattern that avoids waterfall renders and manual loading state management.
Use Server Components by default
Server Components reduce client bundle size and enable direct database access. Only add 'use client' when you need interactivity (event handlers, hooks, browser APIs).
"use client";
// This component only renders data — no interactivity
export default function UserList({ users }) {
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}// No "use client" needed — this is a Server Component
export default async function UserList() {
const users = await db.from("users").select("*");
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}Use the Metadata API for SEO
Export a metadata object or generateMetadata function from page/layout files instead of using <head> tags manually. This enables streaming and proper deduplication.
export default function Page() {
return (
<>
<head><title>My Page</title></head>
<main>...</main>
</>
);
}export const metadata: Metadata = {
title: "My Page",
description: "Page description for SEO",
};
export default function Page() {
return <main>...</main>;
}Fetch data on the server when possible
Client-side data fetching with useEffect adds waterfall latency and loading spinners. In the App Router, fetch data in Server Components or Server Actions.
"use client";
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/data").then(r => r.json()).then(setData);
}, []);// Server Component — data is fetched at request time
export default async function Page() {
const data = await getData();
return <DataView data={data} />;
}Validate request body in API route handlers
API route handlers (route.ts) receive raw request data. Always parse and validate the body with a schema before processing.
export async function POST(req: Request) {
const body = await req.json();
await db.insert(body); // unvalidated
}export async function POST(req: Request) {
const body = schema.parse(await req.json());
await db.insert(body);
}Add loading.tsx and error.tsx to route segments
Missing loading and error files cause raw spinners or unhandled crashes. Add loading.tsx for streaming UI and error.tsx for graceful error recovery in each major route segment.
Use parallel data fetching to avoid waterfalls
Sequential awaits in Server Components create request waterfalls. Use Promise.all or parallel route segments to load independent data concurrently.
export default async function Page() {
const user = await getUser();
const posts = await getPosts(); // waits for user first
const comments = await getComments(); // waits for posts
}export default async function Page() {
const [user, posts, comments] = await Promise.all([
getUser(),
getPosts(),
getComments(),
]);
}Handle process signals for graceful shutdown
Long-running services should handle SIGTERM and SIGINT to close database connections, finish in-flight requests, and flush logs before exiting.
process.on("SIGTERM", async () => {
await server.close();
await db.end();
process.exit(0);
});Use structured logging instead of console.log
Structured JSON logging (pino, winston) enables filtering, searching, and alerting in production. Raw console.log is unstructured and hard to parse at scale.
console.log("User " + userId + " logged in");log.info("User logged in", { userId, source: "auth" });Validate environment variables at startup
Missing or malformed env vars cause runtime crashes in production. Validate all required env vars at application startup with a schema.
const apiKey = process.env.API_KEY!; // crashes at runtime if missingconst env = z.object({
API_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
}).parse(process.env);Use AbortController for cancellable fetch requests
Long-running fetch requests without timeouts can hang indefinitely. Use AbortController with a timeout to cancel stale requests.
const res = await fetch(url); // no timeoutconst controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
const res = await fetch(url, { signal: controller.signal });Python (15 practices)
Never use pickle for untrusted data
pickle.loads() executes arbitrary Python code during deserialization. Use JSON, MessagePack, or Protocol Buffers for untrusted data.
import pickle
data = pickle.loads(user_input)import json
data = json.loads(user_input)Use parameterized SQL queries
String formatting in SQL queries enables SQL injection. Use parameterized queries with placeholders.
cursor.execute(f"SELECT * FROM users WHERE id = '{user_id}'")cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))Validate file paths to prevent traversal
User-supplied paths can escape the intended directory with ../ sequences. Use pathlib to resolve and validate paths.
filepath = os.path.join(upload_dir, request.args["filename"])
with open(filepath) as f:
return f.read()filepath = (Path(upload_dir) / request.args["filename"]).resolve()
if not str(filepath).startswith(str(Path(upload_dir).resolve())):
raise ValueError("Invalid path")
with open(filepath) as f:
return f.read()Avoid shell=True in subprocess calls
subprocess with shell=True passes the command through the shell, enabling command injection. Use a list of arguments instead.
subprocess.run(f"convert {user_filename}", shell=True)subprocess.run(["convert", user_filename], shell=False)Use secrets module for cryptographic randomness
The random module is not cryptographically secure. Use secrets for tokens, passwords, and security-sensitive random values.
import random
token = ''.join(random.choices(string.ascii_letters, k=32))import secrets
token = secrets.token_urlsafe(32)Use yaml.safe_load instead of yaml.load
yaml.load() can execute arbitrary Python objects. Always use yaml.safe_load() for untrusted YAML input.
import yaml
data = yaml.load(user_input)import yaml
data = yaml.safe_load(user_input)Use generators for large datasets
Loading entire datasets into memory causes OOM errors. Use generators and itertools for lazy evaluation of large sequences.
results = [transform(item) for item in get_all_records()] # loads all into memorydef process_records():
for item in get_all_records():
yield transform(item)Avoid global mutable state
Global mutable variables cause concurrency bugs and make testing difficult. Use dependency injection, function parameters, or context managers.
db_connection = None # global mutable
def get_users():
return db_connection.query("SELECT * FROM users")def get_users(db: Connection) -> list[User]:
return db.query("SELECT * FROM users")Use type hints for function signatures
Type hints improve readability, enable static analysis with mypy, and serve as documentation. Annotate function parameters and return types.
def process_order(order, user, discount=None):
passdef process_order(order: Order, user: User, discount: Decimal | None = None) -> OrderResult:
passUse context managers for resource cleanup
Files, database connections, and locks must be properly closed. Context managers (with statements) ensure cleanup even when exceptions occur.
f = open("data.csv")
data = f.read()
f.close() # skipped if exception occurswith open("data.csv") as f:
data = f.read()Use pytest fixtures for test setup
Fixtures provide reusable, composable test setup and teardown. Use fixtures instead of setUp/tearDown methods or repeated inline setup.
@pytest.fixture
def db():
conn = create_test_db()
yield conn
conn.close()
def test_create_user(db):
user = create_user(db, "Alice")
assert user.name == "Alice"Never disable CSRF protection
Django's CSRF middleware prevents cross-site request forgery. Never use @csrf_exempt on views that modify data unless they use token-based auth (e.g., API endpoints with JWT).
@csrf_exempt
def update_profile(request):
# No CSRF protection!def update_profile(request):
# CSRF token validated automatically by middlewareAlways validate serializer input before saving
Call .is_valid(raise_exception=True) on DRF serializers before accessing .validated_data or calling .save().
serializer = UserSerializer(data=request.data)
serializer.save() # no validation!serializer = UserSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()Never run Flask with debug=True in production
Flask debug mode exposes an interactive debugger that allows arbitrary code execution. Ensure DEBUG is False in production.
app.run(debug=True) # NEVER in productionapp.run(debug=os.getenv("FLASK_DEBUG", "false").lower() == "true")Go (10 practices)
Validate all inputs at handler boundaries
HTTP handlers should validate and parse input immediately. Use a validation library or custom validation before processing.
func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
user, _ := db.GetUser(id) // unvalidated
}func handler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if _, err := uuid.Parse(id); err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := db.GetUser(id)
}Use crypto/rand, not math/rand for security-sensitive values
math/rand produces predictable sequences. Use crypto/rand for tokens, keys, nonces, and any security-sensitive random values.
import "math/rand"
token := fmt.Sprintf("%d", rand.Int())import "crypto/rand"
b := make([]byte, 32)
crypto_rand.Read(b)
token := base64.URLEncoding.EncodeToString(b)Never ignore errors — always handle or propagate
Ignoring errors with _ hides failures that can lead to data corruption, security bypasses, or silent data loss. Always check and handle every error.
result, _ := db.Query("SELECT * FROM users")
// error silently ignoredresult, err := db.Query("SELECT * FROM users")
if err != nil {
return fmt.Errorf("query users: %w", err)
}Use parameterized queries with database/sql
String formatting in SQL queries creates injection vulnerabilities. Use placeholder arguments with db.Query or db.Exec.
db.Query(fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", name))db.Query("SELECT * FROM users WHERE name = $1", name)Prevent goroutine leaks with context cancellation
Goroutines that block forever on channels or I/O without cancellation leak memory. Use context.WithCancel or context.WithTimeout to ensure cleanup.
go func() {
for {
data := <-ch // blocks forever if ch is never closed
process(data)
}
}()go func() {
for {
select {
case data := <-ch:
process(data)
case <-ctx.Done():
return
}
}
}()Use context for cancellation and timeouts
Pass context.Context as the first parameter to functions that do I/O or spawn goroutines. This enables graceful cancellation and timeout propagation.
func FetchData(url string) ([]byte, error) {
resp, err := http.Get(url)
}func FetchData(ctx context.Context, url string) ([]byte, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
}Accept interfaces, return structs
Accepting interfaces makes functions flexible and testable. Returning concrete structs gives callers access to all methods without type assertions.
func NewService() ServiceInterface {
return &service{}
}type Reader interface {
Read(ctx context.Context, id string) (Record, error)
}
func NewService(db Reader) *Service {
return &Service{db: db}
}Wrap errors with context using fmt.Errorf %w
Bare error returns lose context about where failures occurred. Wrap errors with fmt.Errorf and %w to preserve the error chain while adding context.
user, err := db.GetUser(id)
if err != nil {
return err // no context
}user, err := db.GetUser(id)
if err != nil {
return fmt.Errorf("get user %s: %w", id, err)
}Use sync.Pool for frequently allocated objects
Allocating and GC-ing many short-lived objects causes CPU pressure. sync.Pool recycles objects to reduce allocation overhead in hot paths.
var bufPool = sync.Pool{
New: func() any { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()
}Use table-driven tests
Table-driven tests make it easy to add cases and see all scenarios at a glance. Use t.Run for subtest naming and parallel execution.
tests := []struct {
name string
input string
want int
}{
{"empty", "", 0},
{"hello", "hello", 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Len(tt.input)
if got != tt.want {
t.Errorf("Len(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}Rust (5 practices)
Minimize and document unsafe blocks
Unsafe code bypasses Rust's safety guarantees. Isolate unsafe code into small, well-documented functions with safety invariants documented in comments.
unsafe {
// 50 lines of complex logic
let ptr = some_ffi_call();
// ... more unsafe operations
}/// # Safety
/// `ptr` must be a valid, aligned pointer to an initialized `Foo`.
unsafe fn deref_foo(ptr: *const Foo) -> &Foo {
&*ptr
}Avoid unnecessary clones — use references and borrowing
Cloning large data structures is expensive. Use references (&T) and borrowing to share data without copying. Clone only when ownership transfer is genuinely needed.
fn process(data: Vec<String>) {
let copy = data.clone(); // unnecessary clone
for item in © { ... }
}fn process(data: &[String]) {
for item in data { ... }
}Use Result<T, E> with thiserror for typed errors
Use the type system for error handling instead of panicking. Define error enums with thiserror for clean, descriptive error types.
fn parse_config(path: &str) -> Config {
let data = std::fs::read_to_string(path).unwrap(); // panics
serde_json::from_str(&data).unwrap() // panics
}#[derive(thiserror::Error, Debug)]
enum ConfigError {
#[error("failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("invalid config format: {0}")]
Parse(#[from] serde_json::Error),
}
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let data = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&data)?)
}Use newtype pattern for domain types
Wrapping primitive types in newtypes prevents mixing up values with the same underlying type (e.g., UserId vs OrderId) and enables custom validation.
struct UserId(uuid::Uuid);
struct OrderId(uuid::Uuid);
// These types cannot be accidentally mixed up
fn get_user_orders(user_id: UserId) -> Vec<OrderId> { ... }Prefer iterator chains over manual loops
Iterator chains are zero-cost abstractions that enable the compiler to optimize better than manual loops with mutable accumulators.
let mut result = Vec::new();
for item in &items {
if item.active {
result.push(item.name.to_uppercase());
}
}let result: Vec<_> = items.iter()
.filter(|item| item.active)
.map(|item| item.name.to_uppercase())
.collect();Ruby (4 practices)
Use strong parameters to prevent mass assignment
Passing raw params to create/update allows attackers to set any attribute. Use strong parameters to whitelist allowed fields.
User.create(params[:user]) # all fields allowedUser.create(user_params)
def user_params
params.require(:user).permit(:name, :email)
endNever interpolate user input in ActiveRecord queries
String interpolation in where clauses bypasses ActiveRecord's parameterization. Use hash conditions or array conditions.
User.where("name = '#{params[:name]}'")User.where(name: params[:name])
# or
User.where("name = ?", params[:name])Use eager loading to prevent N+1 queries
Accessing associations in loops triggers a query per record. Use includes, preload, or eager_load to batch-load associations.
@posts = Post.all
# In view: post.comments.count triggers N+1@posts = Post.includes(:comments).allKeep protect_from_forgery enabled
Rails CSRF protection is enabled by default. Never skip it for form-based endpoints. API-only controllers should use token-based auth instead.
Java (4 practices)
Avoid Java native deserialization of untrusted data
ObjectInputStream.readObject() can execute arbitrary code. Use JSON, Protocol Buffers, or a safe deserialization library for untrusted input.
Use PreparedStatement, never string concatenation in SQL
Building SQL with string concatenation enables injection. Use PreparedStatement with parameterized queries.
Statement stmt = conn.createStatement();
stmt.executeQuery("SELECT * FROM users WHERE id = '" + userId + "'");PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setString(1, userId);Prefer immutable objects
Immutable objects are thread-safe, easier to reason about, and prevent unintended side effects. Use records (Java 16+) or final fields with no setters.
public class User {
public String name;
public String email;
}public record User(String name, String email) {}Never catch generic Exception or Throwable
Catching Exception swallows unexpected errors including NullPointerException and OutOfMemoryError. Catch specific exception types and handle them appropriately.
try {
processOrder();
} catch (Exception e) {
// swallows everything
}try {
processOrder();
} catch (PaymentException e) {
handlePaymentFailure(e);
} catch (InventoryException e) {
handleOutOfStock(e);
}SQL / Database (15 practices)
Always use parameterized queries
SQL injection is the most exploited web vulnerability. Never concatenate user input into SQL strings. Use parameterized queries, prepared statements, or ORM query builders.
Enable Row Level Security on all tables
Tables without RLS policies are accessible to any authenticated user via the Supabase client. Enable RLS and define explicit policies for each table.
CREATE TABLE documents (
id uuid PRIMARY KEY,
content text
);
-- No RLS enabled!CREATE TABLE documents (
id uuid PRIMARY KEY,
content text,
org_id uuid NOT NULL
);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_access" ON documents
USING (org_id = (SELECT org_id FROM profiles WHERE id = auth.uid()));Use least-privilege database roles
Application database users should have only the permissions they need. Never connect with superuser or admin credentials from application code.
Add indexes on foreign key columns
Foreign keys without indexes cause full table scans on joins and cascading deletes. Add an index on every foreign key column.
CREATE TABLE posts (
id uuid PRIMARY KEY,
user_id uuid REFERENCES users(id)
);
-- No index on user_id!CREATE TABLE posts (
id uuid PRIMARY KEY,
user_id uuid REFERENCES users(id)
);
CREATE INDEX idx_posts_user_id ON posts(user_id);Avoid SELECT * in production queries
SELECT * fetches all columns including large text/blob fields and prevents index-only scans. Select only the columns you need.
SELECT * FROM users WHERE id = $1;SELECT id, name, email FROM users WHERE id = $1;Use EXPLAIN ANALYZE on slow queries
Before optimizing, understand the query plan. EXPLAIN ANALYZE shows actual execution times, row counts, and whether indexes are used.
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'pending' AND created_at > now() - interval '7 days';Use cursor-based pagination for large datasets
OFFSET-based pagination becomes slow on large tables because the database must skip N rows. Use keyset (cursor) pagination with WHERE + LIMIT.
SELECT * FROM products ORDER BY created_at LIMIT 20 OFFSET 10000;SELECT * FROM products
WHERE created_at < $cursor
ORDER BY created_at DESC
LIMIT 20;Use batch inserts instead of row-by-row
Inserting rows one at a time in a loop creates massive overhead from round trips and transaction commits. Batch inserts into a single statement.
for item in items:
cursor.execute("INSERT INTO logs (msg) VALUES (%s)", (item,))cursor.executemany(
"INSERT INTO logs (msg) VALUES (%s)",
[(item,) for item in items]
)Use RPC functions for atomic multi-table operations
Multiple sequential Supabase queries are not atomic. Wrap multi-table operations in a PostgreSQL function and call it via .rpc() for transactional safety.
await supabase.from("orders").insert(order);
await supabase.from("inventory").update(inv); // fails: order exists without inventory update// PostgreSQL function handles both in a transaction
await supabase.rpc("create_order_with_inventory", {
order_data: order,
inventory_update: inv,
});Avoid .single() on queries that may return multiple rows
The .single() modifier throws an error if the query returns more than one row. Use .maybeSingle() for optional lookups and .limit(1) when you want just the first match.
const { data } = await supabase
.from("users")
.select("*")
.eq("email", email)
.single(); // throws if duplicates existconst { data } = await supabase
.from("users")
.select("*")
.eq("email", email)
.maybeSingle(); // returns null if not found, no error on missingNever trust client-side RLS for admin operations
RLS policies protect against unauthorized access, but admin mutations (e.g., org-wide updates) should use the service_role client to bypass RLS safely on the server.
// Client component doing admin operation through RLS
const supabase = createClient();
await supabase.from("org_settings").update({ plan: "enterprise" });// Server action using admin client
const admin = createAdminClient();
await admin.from("org_settings").update({ plan: "enterprise" }).eq("org_id", orgId);Use composite indexes for multi-column WHERE clauses
Queries filtering on multiple columns benefit from a single composite index rather than separate single-column indexes.
CREATE INDEX idx_orders_status_date ON orders(status, created_at DESC);Write backward-compatible migrations
Migrations that rename or drop columns break running application instances during deployment. Use a two-phase approach: add new column, migrate data, then remove old column in a later release.
Use connection pooling for application databases
Opening a new connection per query is expensive. Use a connection pool (PgBouncer, built-in pool) to reuse connections efficiently.
Include created_at and updated_at on every table
Timestamps enable debugging, auditing, and cache invalidation. Use database defaults and triggers to ensure they are always populated.
CREATE TABLE items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE TRIGGER items_updated_at
BEFORE UPDATE ON items
FOR EACH ROW EXECUTE FUNCTION moddatetime(updated_at);General (21 practices)
Enforce access control on every endpoint
Every API endpoint and server action must verify authentication and authorization before processing. Missing checks allow privilege escalation and unauthorized data access.
Use strong, modern cryptographic algorithms
MD5 and SHA1 are broken for security purposes. Use SHA-256+, bcrypt/scrypt/argon2 for passwords, and AES-256-GCM for encryption.
Treat all user input as untrusted
Input from HTTP parameters, headers, cookies, file uploads, and third-party APIs must be validated and sanitized before use in queries, commands, or rendering.
Implement rate limiting on authentication endpoints
Without rate limiting, authentication endpoints are vulnerable to brute force and credential stuffing attacks. Implement progressive delays or lockouts.
Log security events and monitor for anomalies
Log authentication attempts, authorization failures, input validation failures, and admin actions. Never log secrets, passwords, or PII.
Validate URLs before server-side fetching
When the server fetches a user-supplied URL, validate the scheme (allow only https), resolve the hostname, and block private/internal IP ranges to prevent SSRF.
Never hardcode secrets in source code
API keys, passwords, and tokens in source code get committed to version control and are visible in CI logs. Use environment variables or a secrets manager.
Audit dependencies for known vulnerabilities
Run npm audit, pip-audit, or govulncheck regularly. Pin dependency versions and review changelogs before upgrading major versions.
Use dependency injection for testability
Hardcoded dependencies make code untestable and inflexible. Pass dependencies as constructor/function parameters so they can be mocked in tests and swapped in different environments.
class OrderService {
private db = new PostgresDB(); // hardcoded
async getOrder(id: string) { ... }
}class OrderService {
constructor(private db: Database) {} // injected
async getOrder(id: string) { ... }
}Separate business logic from infrastructure
Business rules should not depend on HTTP frameworks, databases, or file systems. Keep core logic in pure functions and use adapters for I/O.
Single Responsibility — one reason to change per module
A module that handles parsing, validation, business logic, and database access has too many reasons to change. Split into focused modules with clear boundaries.
Open-Closed Principle — extend behavior without modifying code
Use composition, strategy patterns, and plugin architectures to add new behavior without changing existing code. This reduces regression risk.
Keep files under 500 lines
Files over 500 lines are hard to navigate, review, and test. Extract related functionality into separate modules when files grow beyond this threshold.
Test behavior, not implementation details
Tests that assert on internal state or mock every dependency break on refactoring. Test the public interface and observable behavior.
// Tests implementation details
expect(service._internalCache.size).toBe(5);
expect(mockDb.query).toHaveBeenCalledTimes(3);// Tests observable behavior
const result = await service.getTopProducts(5);
expect(result).toHaveLength(5);
expect(result[0].name).toBe("Widget");Mock at system boundaries, not everywhere
Only mock external dependencies (databases, APIs, file systems). Let internal modules collaborate naturally to catch integration issues.
Structure tests with Arrange-Act-Assert
Each test should have three clear sections: set up preconditions (Arrange), execute the action (Act), and verify the outcome (Assert). This makes tests readable and maintainable.
test("creates user with hashed password", async () => {
// Arrange
const input = { email: "test@example.com", password: "secret123" };
// Act
const user = await createUser(input);
// Assert
expect(user.email).toBe("test@example.com");
expect(user.password_hash).not.toBe("secret123");
});Make tests deterministic — no flaky tests
Tests that depend on timing, network calls, random values, or shared state produce intermittent failures. Use fixed seeds, fake clocks, and isolated test databases.
Cover error paths, not just happy paths
Most bugs live in error handling, edge cases, and boundary conditions. Write tests for invalid input, network failures, empty collections, and concurrent access.
Provide text alternatives for all non-text content
Images need alt text, icons need aria-labels, and interactive elements need accessible names. Screen readers cannot interpret visual content without text alternatives.
<button><TrashIcon /></button>
<img src="chart.png" /><button aria-label="Delete item"><TrashIcon /></button>
<img src="chart.png" alt="Sales chart showing 20% growth in Q3" />Ensure full keyboard navigation
All interactive elements must be reachable and operable with keyboard alone. Use semantic HTML elements (button, a, input) which have built-in keyboard support.
<div onClick={handleClick} className="button-like">Click me</div><button onClick={handleClick}>Click me</button>Maintain WCAG color contrast ratios
Text must have at least 4.5:1 contrast ratio against its background (3:1 for large text). Use tools like the WebAIM contrast checker to verify.
OWASP Top 10 (2021) mapping
Security practices in this knowledge base are mapped to the OWASP Top 10 categories. The AI review engine uses these mappings to classify findings and provide authoritative references.
A01: Broken Access Control
Failures that allow users to act outside their intended permissions, including unauthorized data access, privilege escalation, and CORS misconfigurations.
Related practices
[TypeScript] Use Server Actions or CSRF tokens for mutations[TypeScript] Validate file paths to prevent directory traversal[Python] Validate file paths to prevent traversal[Python] Never disable CSRF protection[SQL / Database] Enable Row Level Security on all tables[SQL / Database] Use least-privilege database roles[General] Enforce access control on every endpoint[Ruby] Keep protect_from_forgery enabledA02: Cryptographic Failures
Weak or missing encryption for data in transit and at rest, use of deprecated algorithms, and improper key management.
Related practices
[TypeScript] Never expose secrets in client-side code[Python] Use secrets module for cryptographic randomness[Go] Use crypto/rand, not math/rand for security-sensitive values[General] Use strong, modern cryptographic algorithmsA03: Injection
SQL injection, NoSQL injection, OS command injection, LDAP injection, and cross-site scripting (XSS) where user input is not validated or sanitized.
Related practices
[TypeScript] Never use eval() or Function() constructor[TypeScript] Sanitize dangerouslySetInnerHTML input[TypeScript] Validate all user input at system boundaries[TypeScript] Use parameterized queries, never string interpolation[TypeScript] Guard against prototype pollution[TypeScript] Avoid catastrophic backtracking in regular expressions[TypeScript] Configure Content Security Policy headers[TypeScript] Validate request body in API route handlers[Python] Use parameterized SQL queries[Python] Avoid shell=True in subprocess calls[Python] Always validate serializer input before saving[Go] Validate all inputs at handler boundaries[Go] Use parameterized queries with database/sql[SQL / Database] Always use parameterized queries[General] Treat all user input as untrusted[Ruby] Never interpolate user input in ActiveRecord queries[Java] Use PreparedStatement, never string concatenation in SQLA04: Insecure Design
Missing or ineffective control design, failure to use secure design patterns, and missing threat modeling.
OWASP referenceA05: Security Misconfiguration
Missing security hardening, improperly configured permissions, unnecessary features enabled, default credentials, and verbose error handling.
Related practices
[TypeScript] Never expose secrets in client-side code[TypeScript] Configure Content Security Policy headers[Python] Set secure cookie flags in production[Python] Never run Flask with debug=True in production[General] Never hardcode secrets in source codeA06: Vulnerable and Outdated Components
Using components with known vulnerabilities, unsupported software, and failure to track and update dependencies.
Related practices
[General] Audit dependencies for known vulnerabilitiesA07: Identification and Authentication Failures
Weak authentication mechanisms, credential stuffing, brute force attacks, session fixation, and missing multi-factor authentication.
Related practices
[General] Implement rate limiting on authentication endpointsA08: Software and Data Integrity Failures
Code and infrastructure that does not protect against integrity violations, including insecure CI/CD pipelines, auto-update without verification, and insecure deserialization.
Related practices
[Python] Never use pickle for untrusted data[Python] Use yaml.safe_load instead of yaml.load[Ruby] Use strong parameters to prevent mass assignment[Java] Avoid Java native deserialization of untrusted dataA09: Security Logging and Monitoring Failures
Insufficient logging, monitoring, and alerting that prevents detection of active breaches and incident response.
Related practices
[General] Log security events and monitor for anomaliesA10: Server-Side Request Forgery (SSRF)
Application fetches a remote resource without validating the user-supplied URL, allowing attackers to coerce the application to send requests to unexpected destinations.
Related practices
[General] Validate URLs before server-side fetchingLet AI review with best practices
The BeforeMerge AI engine references this knowledge base during every review, giving your team specific, authoritative feedback instead of generic advice.