About HowToCodePage

Learn to code, one page at a time.

HowToCodePage is an independent, ad-light reference for the practical corners of software development — the encodings, hashes, formats, and time math that show up constantly but are easy to get subtly wrong. Every reference page is written as an original synthesis, not a copy of any single source.

FAQ

Is HowToCodePage a replacement for official docs?
No. It is a learning companion with original, synthesized explanations. For authoritative detail, follow the links to primary sources (RFC, ECMAScript, Unicode, MDN).
Are the tools safe to paste sensitive data into?
Yes for confidentiality of transport: all processing happens in your browser tab; nothing is sent to a server. But do not paste secrets you would not want in the page's memory.
How is the content kept current?
The site is rebuilt and redeployed automatically on a schedule; reference pages carry a 'last refreshed' date.

Changelog

Launch 2026-09-17 — initial reference set and browser-only tools.

"},{"title":"Functions and scope","path":"/learn/js-functions/","kind":"Learn","text":"Functions and scope Parameters, defaults, arrow functions, closures and this — the ideas behind almost every JavaScript interview question. Ways to write one function add(a, b = 0) { return a + b; } // arrow function: concise, and does not rebind `this` const mult = (a, b) => a * b; const double = n => n * 2; // one param, no parens needed const makeObj = id => ({ id }); // wrap object literal in parens // rest parameters collect the remainder function sum(...nums) { return nums.reduce((t, n) => t + n, 0); } Arrow functions inherit this from where they were defined. That makes them ideal for callbacks, and wrong for object methods that need their own this . Scope and closures let and const are block-scoped; var is function-scoped. A closure is simply a function that keeps access to variables from the scope where it was created, even after that scope has returned. function makeCounter() { let n = 0; // private to each counter return () => ++n; } const next = makeCounter(); next(); // 1 next(); // 2 Creating closures inside a loop with var captures one shared variable — the classic 'all values are the last value' bug. Use let , which creates a fresh binding per iteration. Understanding this obj.method() the object before the dot plain fn() undefined in strict mode new Fn() the new instance arrow function inherited from enclosing scope fn.call/apply/bind whatever you pass const obj = { n: 1, inc() { this.n += 1; } // method shorthand: this === obj }; const inc = obj.inc; inc(); // TypeError - lost its receiver const safe = obj.inc.bind(obj); // permanently attached safe();"},{"title":"Arrays and iteration","path":"/learn/js-arrays/","kind":"Learn","text":"Arrays and iteration The methods that replace most for-loops — map, filter, reduce, find — and when mutation versus copying matters. Transformation methods const nums = [4, 1, 8, 3]; nums.map(n => n * 2); // [8, 2, 16, 6] same length nums.filter(n => n > 3); // [4, 8] subset nums.find(n => n > 3); // 4 first match nums.findIndex(n => n > 3); // 0 nums.reduce((t, n) => t + n, 0); // 16 collapse to one value nums.some(n => n > 7); // true nums.every(n => n > 0); // true map/filter/slice/concat new array No push/pop/shift/unshift length/element Yes splice removed items Yes sort/reverse the array itself Yes reduce anything Depends on your callback sort() converts elements to strings by default, so [10, 9].sort() gives [10, 9] . Always pass a comparator: arr.sort((a, b) => a - b) . Looping choices for (const item of nums) console.log(item); // values for (const [i, item] of nums.entries()) console.log(i, item); nums.forEach(n => console.log(n)); // no break/continue // never use for...in for arrays - it walks enumerable keys for (const k in nums) console.log(k); // '0','1',... plus inherited surprises for…of for values, supports break/await. for…in enumerates keys and is meant for plain objects. forEach cannot be stopped cleanly — use some/every to exit early. Useful patterns const unique = [...new Set(arr)]; const flat = nested.flat(2); const chunks = Array.from({ length: Math.ceil(a.length / 10) }, (_, i) => a.slice(i * 10, i * 10 + 10)); const grouped = Object.groupBy(items, x => x.type); // modern runtimes // shallow copy vs mutation const sorted = [...nums].sort((a, b) => a - b); // keeps nums intact"},{"title":"Objects and destructuring","path":"/learn/js-objects/","kind":"Learn","text":"Objects and destructuring Property shorthand, spread merging, optional chaining, and copying objects without the reference traps. Short modern syntax const id = 7, active = true; const user = { id, active }; // property shorthand const user2 = { id, role: 'admin', login() { return this.id; } }; const { id: userId, role = 'guest' } = user2; // rename + default const copy = { ...user2, role: 'owner' }; // spread override Computed keys: { [key]: value }. Optional chaining: user?.profile?.email — short-circuits instead of throwing. Nullish coalescing: x ?? 'default' — unlike || it keeps 0 and ''. References and copies Objects are copied by reference : assigning one does not clone it, so both variables point at the same data. const a = { tags: ['x'] }; const b = a; b.tags.push('y'); console.log(a.tags); // ['x','y'] - same object const shallow = { ...a }; // top level copied only const deep = structuredClone(a); // true deep copy (modern runtimes) Spread is a shallow copy — nested objects still share references. Use structuredClone for a genuine deep copy (it handles Dates, Maps and Sets; unlike JSON.parse(JSON.stringify()) , which silently mangles them). Object vs Map Object Fixed known shape, JSON interchange, simple records Map Dynamic keys, frequent add/remove, non-string keys, insertion order matters const m = new Map(); m.set(user, 'cached'); // any key type m.size; // no Object.keys length dance for (const [k, v] of m) console.log(k, v);"},{"title":"Working with the DOM","path":"/learn/js-dom/","kind":"Learn","text":"Working with the DOM Selecting elements, reading and writing content safely, creating nodes, and why innerHTML deserves caution. Finding elements document.querySelector('.card'); // first match const all = document.querySelectorAll('.card'); // static NodeList all.forEach(el => el.classList.add('seen')); // cache a reference rather than re-querying in loops const form = document.querySelector('#signup'); querySelectorAll returns a static NodeList; getElementsByClassName returns a live HTMLCollection that updates as the DOM changes — a common source of confusion while looping. Reading and writing textContent Text only — safest choice innerHTML Parsed HTML — powerful, XSS risk value Form control's current value classList Add/remove/toggle classes setAttribute Any attribute el.textContent = userInput; // rendered as plain text el.setAttribute('aria-expanded', 'false'); el.dataset.userId = '42'; // data-user-id // style: prefer classes over inline styles el.classList.toggle('is-open', open); el.style.setProperty('--accent', '#4f46e5'); Never assign untrusted input to innerHTML — el.innerHTML = name is an XSS hole as soon as name contains a script-bearing tag. Use textContent , or sanitize with a trusted library. Creating and inserting const li = document.createElement('li'); li.className = 'row'; li.textContent = ' item '; list.append(li); // or prepend / before / after // efficient bulk insert const frag = document.createDocumentFragment(); items.forEach(i => frag.append(makeRow(i))); list.append(frag); // single reflow li.remove(); Batch DOM writes inside a fragment (or build one HTML string once) rather than appending in a loop — every insertion can trigger layout. Timing your code Put <script> near the end of the body, or use defer so the script runs after parsing. type='module' is deferred by default — no need to add defer. async executes as soon as it downloads; order is not guaranteed. "},{"title":"Events","path":"/learn/js-events/","kind":"Learn","text":"Events addEventListener, bubbling and capturing, delegation for dynamic lists, and forms without page reloads. Listening btn.addEventListener('click', event => { console.log(event.target); // what was clicked console.log(event.currentTarget); // where the listener lives }); // remove requires the same function reference function handler() {} el.addEventListener('click', handler); el.removeEventListener('click', handler); click Mouse click or keyboard activation input Value changes as the user types change Value committed (blur for text) submit Form submitted keydown Key pressed DOMContentLoaded HTML parsed Bubbling and capturing An event travels three phases: capture down from the root, the target , then bubble back up. Listeners default to the bubble phase. el.addEventListener('click', fn); // bubble (default) el.addEventListener('click', fn, true); // capture el.addEventListener('click', fn, { once: true, passive: true }); event.stopPropagation(); // stop further travel event.preventDefault(); // stop the default action stopPropagation breaks analytics and delegated handlers globally. Prefer checking event.target in a delegate handler instead. Event delegation Instead of attaching a listener to every row, attach one to a stable parent and ask what was clicked. This works for elements added later and uses far less memory. list.addEventListener('click', e => { const btn = e.target.closest('[data-action]'); if (!btn) return; // click was elsewhere handle(btn.dataset.action, btn.dataset.id); }); Forms without reload form.addEventListener('submit', async e => { e.preventDefault(); // stop navigation const data = new FormData(form); const payload = Object.fromEntries(data); await fetch(form.action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); }); Bind to the form's submit , not the button's click — you keep Enter-key behaviour and validation for free."},{"title":"Async JavaScript and fetch","path":"/learn/js-fetch-async/","kind":"Learn","text":"Async JavaScript and fetch Promises, async/await, real fetch usage with error handling, cancellation, and running tasks concurrently. Promises in one page A promise is a placeholder for a value that is not ready yet. It settles once — either fulfilled with a value, or rejected with a reason. p .then(value => transform(value)) .catch(err => console.error(err)) .finally(() => hideSpinner()); // same thing, flatter async function run() { try { const value = await p; return transform(value); } catch (err) { console.error(err); } finally { hideSpinner(); } } await only works inside an async function (or at the top level of an ES module). Top-level await is supported in modules but blocks importing modules until it settles. fetch, properly async function getJson(url) { const res = await fetch(url, { headers: { Accept: 'application/json' }, credentials: 'same-origin' }); if (!res.ok) { // fetch does NOT throw on 404/500 throw new Error('HTTP ' + res.status); } return res.json(); } The single most common fetch bug: it rejects only on network failure, so a 500 response resolves happily. Always check res.ok before reading the body. await fetch('/api/item', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'new' }) }); // timeouts with AbortController const ac = new AbortController(); setTimeout(() => ac.abort(), 8000); const res = await fetch(url, { signal: ac.signal }); Concurrency patterns // sequential - each waits for the previous for (const id of ids) results.push(await get(id)); // parallel - start all, wait for all const results = await Promise.all(ids.map(get)); // tolerate individual failures const settled = await Promise.allSettled(ids.map(get)); const ok = settled.filter(r => r.status === 'fulfilled').map(r => r.value); // race: first to settle const fastest = await Promise.any([fetch(a), fetch(b)]); Promise.all rejects entirely on the first failure. Use allSettled when partial success is acceptable — dashboards, feeds, bulk imports. await inside loops forEach cannot await properly — the callbacks start, but nothing waits for them. Use for…of for sequential work, or map into Promise.all for parallel. // wrong: fire and forget items.forEach(async i => { await save(i); }); // right (sequential) for (const i of items) await save(i); // right (parallel) await Promise.all(items.map(save));"},{"title":"Python: getting started","path":"/learn/python-intro/","kind":"Learn","text":"Python: getting started Running Python, indentation as syntax, variables, and the built-in types you touch in every script. Running code python --version python hello.py # interactive REPL - the fastest way to experiment python >>> 2 + 3 5 Use python3 on macOS/Linux if python still points at Python 2 (rare now, but check). Create an isolated environment per project: python -m venv .venv then activate it. source .venv/bin/activate (macOS/Linux) or .venv\\Scripts\\activate (Windows). Never install packages globally with sudo pip install . Virtual environments prevent one project's pins from breaking another's. Indentation is syntax Python uses indentation instead of braces to delimit blocks. Four spaces per level is the convention — and mixing tabs with spaces is a syntax error, so configure your editor to insert spaces. score = 85 if score >= 90: grade = 'A' elif score >= 80: grade = 'B' else: grade = 'C' print(grade) # B Inconsistent indentation raises IndentationError before your code runs at all. If a block looks right but fails, check for a stray tab. Core types int 42 Arbitrary precision — no overflow float 3.14 IEEE 754 double str 'hi', \"hi\" Immutable sequence bool True Capitalized list [1, 2] Mutable ordered dict {'a': 1} Key/value map tuple (1, 2) Immutable ordered NoneType None Absence of a value x, y = 1, 2 # multiple assignment x, y = y, x # swap without a temp n = 10 print(f'n is {n}') # f-strings: the modern way to format print(type(n).__name__) There is no ++ in Python. n++ is silently parsed as two unary plus operators, doing nothing — write n += 1 . Truthiness and None False, None, 0, 0.0 any non-zero number '', [], {}, (), set() any non-empty container Check for None with is None , never == None . Use is not None when 0 or '' are legitimate values you must not skip."},{"title":"Strings","path":"/learn/python-strings/","kind":"Learn","text":"Strings Slicing, f-strings, the methods you actually use, and why joins beat concatenation in loops. Creating and slicing s = 'Python' print(s[0]) # P s[-1] # n negative index counts from the end s[0:2] # 'Py' end index is exclusive s[::-1] # 'nohtyP' reversed len(s) # 6 print('py' in s.lower()) # True - membership test Strings are immutable — every \"change\" creates a new one. That is why s[0] = 'p' raises TypeError . Methods worth memorizing s.strip() Removes leading/trailing whitespace s.lower()/s.upper() Case folding s.split(',') List of parts ','.join(parts) Inverse of split s.replace(a, b) All occurrences s.startswith(p) Boolean prefix test s.find(p) Index or -1 (no exception) csv = ' a, b , c ' fields = [f.strip() for f in csv.strip().split(',')] print(fields) # ['a', 'b', 'c'] path = '/var/log/app.log' path.rsplit('/', 1)[-1] # 'app.log' - split from the right Building strings efficiently # slow: each += allocates a brand new string out = '' for line in lines: out += line + '\\n' # fast: one pass, one allocation out = '\\n'.join(lines) Repeated concatenation inside loops is quadratic in practice. Append to a list and join once — the standard idiom. Text vs bytes Python 3 keeps a clear boundary: str is Unicode text, bytes is raw data. Convert explicitly at the edges of your program — reading files, network sockets. 'café'.encode('utf-8') # b'caf\\xc3\\xa9' b'caf\\xc3\\xa9'.decode('utf-8') # 'café' with open('notes.txt', encoding='utf-8') as f: text = f.read() Always pass encoding= to open() . Omitting it relies on the platform default, so the same code reads fine on Linux and breaks on Windows."},{"title":"Lists, dicts and comprehensions","path":"/learn/python-lists-dicts/","kind":"Learn","text":"Lists, dicts and comprehensions Slicing, mutability gotchas, comprehension syntax, and choosing the right collection for the job. Lists nums = [3, 1, 2] nums.append(4) # [3, 1, 2, 4] nums.extend([5, 6]) nums.insert(0, 0) last = nums.pop() # removes and returns the last item nums.sort() # in place; sorted(nums) returns a copy first, *rest = nums # unpacking Index / append / pop from end O(1) Insert or delete at the front O(n) — use deque x in list O(n) — use a set for repeated checks The aliasing trap Assignment copies the reference , so two names can point at one list. The classic bug is a default mutable argument, which is evaluated once at definition time and shared by every call. a = [1, 2] b = a b.append(3) print(a) # [1, 2, 3] - same object b = a[:] # shallow copy b = list(a) # wrong: the default list is shared between calls def add(item, bucket=[]): bucket.append(item) return bucket # right def add(item, bucket=None): bucket = bucket if bucket is not None else [] bucket.append(item) return bucket [[0]] * 3 creates three references to the same inner list — changing one changes all. Use a comprehension: [[0] for _ in range(3)] . Comprehensions squares = [n * n for n in range(10)] evens = [n for n in nums if n % 2 == 0] # dict comprehension by_id = {u['id']: u for u in users} # conditional transformation labels = ['even' if n % 2 == 0 else 'odd' for n in nums] # set comprehension - deduplicates unique_tags = {t.lower() for t in tags} Read them right to left: the expression first, then the loop, then the filter. If a comprehension needs more than one condition or a nested loop, a plain for block is clearer. Dicts user = {'id': 1, 'name': 'Ada'} user.get('email') # None instead of KeyError user.get('email', 'n/a') # with default user.setdefault('role', 'guest') for key, value in user.items(): print(key, value) merged = {**user, 'role': 'admin'} # Python 3.5+ counts = dict(Counter(words)) # tallying done for you Dicts preserve insertion order (guaranteed since 3.7), so you rarely need OrderedDict anymore. Use collections.defaultdict when building groups."},{"title":"Functions and modules","path":"/learn/python-functions/","kind":"Learn","text":"Functions and modules Arguments, returns, scoping, lambdas, and organizing code into importable modules. Arguments def greet(name, greeting='Hello', *extra, punct='!', **opts): \"\"\"Docstring: what this function is for.\"\"\" parts = [greeting, name, *extra] sep = opts.get('sep', ' ') return sep.join(parts) + punct greet('Ada') # Hello Ada! greet('Ada', punct='?') # keyword-only must be named greet('Ada', 'Hi', punct='.') def f(a, b=1) Positional with default def f(*args) Extra positionals as a tuple def f(**kwargs) Extra keywords as a dict def f(*, a) Keyword-only argument def f(a, /, b) a positional-only Mutable default arguments are evaluated once when the function is defined, then shared by every call. Use None and build inside the body — see the aliasing trap above. Returning values def divide(a, b): return a / b q, r = divmod(10, 3) # multiple return values are a tuple name, _, score = row # _ conventionally means 'ignored' def find(users, uid): return None # explicit absence beats raising for 'not found' A function with no return gives None . Return early for guard clauses rather than nesting — flatter code reads better. Scope Python resolves names with LEGB: Local, Enclosing, Global, Built-in. Assigning inside a function makes a name local unless you declare otherwise. count = 0 def bump(): global count # needed to rebind the module-level name count += 1 def make_adder(n): def add(x): # closure over n return x + n return add add5 = make_adder(5) add5(3) # 8 Modules and imports # maths.py def area(r): return 3.14159 * r * r if __name__ == '__main__': print(area(2)) # only when run directly # consumer from maths import area import maths as m from pathlib import Path The __name__ == '__main__' guard keeps script code from running on import. Prefer importing modules over individual names when several functions share a namespace. Avoid from x import * — it pollutes the namespace and hides where names came from."},{"title":"Errors, files and virtualenvs","path":"/learn/python-errors/","kind":"Learn","text":"Errors, files and virtualenvs Reading tracebacks, using context managers safely, handling JSON, and keeping dependencies isolated. Reading a traceback Tracebacks print oldest call first — scroll to the bottom for the actual exception. The last few frames are almost always your own code, even when the error surfaces inside a library. Traceback (most recent call last): File 'app.py', line 12, in main() File 'app.py', line 8, in main print(items[5]) ~~~~~^^^ IndexError: list index out of range Handling exceptions try: value = int(text) except ValueError as e: print('not a number:', e) else: print('parsed fine', value) # runs if no exception finally: cleanup() # always runs try: risky() except (OSError, ValueError): # tuple of types pass # swallow deliberately, never silently Bare except: also catches KeyboardInterrupt and SystemExit , making programs hard to stop. Catch the narrowest type you can, and log something. Use raise ... from err when re-raising, to preserve the original cause in the traceback chain. Files with context managers with open('data.json', encoding='utf-8') as f: data = json.load(f) # file closed even on error with open('out.txt', 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) for line in pathlib.Path('app.log').read_text(encoding='utf-8').splitlines(): if 'ERROR' in line: print(line) json.load(f) reads from a file object; json.loads(s) parses a string. ensure_ascii=False keeps non-Latin characters readable instead of escaping them. Use pathlib for path building — Path('a') / 'b' works across platforms. Environments and packages python -m venv .venv source .venv/bin/activate # Windows: .venv\\Scripts\\activate pip install requests pip freeze > requirements.txt pip install -r requirements.txt Add .venv/ to .gitignore . Committing an environment breaks every teammate on a different OS."},{"title":"SELECT: reading data","path":"/learn/sql-select/","kind":"Learn","text":"SELECT: reading data Choosing columns, filtering rows, sorting, and limiting — the shape of nearly every query you will write. The shape of a query SQL clauses are written in English order but evaluated differently. Understanding that order explains why aliases work in some clauses and not others. SELECT name, price -- 5. choose columns FROM products -- 1. source table WHERE price > 20 -- 2. filter rows GROUP BY category -- 3. collapse groups HAVING COUNT(*) > 1 -- 4. filter groups ORDER BY price DESC -- 6. sort LIMIT 10; -- 7. restrict rows SELECT * is fine for exploration, terrible for production: it breaks callers when columns change and transfers data you never use. Columns and aliases SELECT DISTINCT category FROM products; SELECT name, price * 1.2 AS price_with_tax FROM products; -- CASE for conditional labels SELECT name, CASE WHEN price >= 100 THEN 'premium' WHEN price >= 20 THEN 'mid' ELSE 'budget' END AS tier FROM products; Because of evaluation order, an alias defined in SELECT cannot be used in WHERE — the filter runs first. Repeat the expression or wrap it in a subquery/CTE. Filtering rows =, <> Equal / not equal (some dialects accept !=) BETWEEN 10 AND 20 Inclusive range IN ('a','b') Any of a list LIKE 'app%' Pattern; % many, _ one IS NULL Null test — never = NULL AND/OR/NOT Combine; AND binds tighter than OR SELECT * FROM users WHERE country = 'US' AND (plan = 'pro' OR trial_ends > CURRENT_DATE) AND email IS NOT NULL; NULL comparisons yield UNKNOWN , not false — so WHERE x <> 5 silently excludes rows where x IS NULL . Add an explicit IS NULL branch when that matters. Sorting and paging SELECT name, price FROM products ORDER BY category ASC, price DESC LIMIT 20 OFFSET 40; -- page 3 Offset paging gets slower as you go deeper — the database still walks skipped rows. For large tables use keyset pagination: WHERE id > :last_id ORDER BY id LIMIT 20. Always include a deterministic tiebreaker (like id) or page boundaries can repeat and skip rows."},{"title":"Joins","path":"/learn/sql-joins/","kind":"Learn","text":"Joins INNER, LEFT, RIGHT and FULL joins explained with row counts — plus the accidental fan-out that ruins aggregates. The four joins INNER JOIN Rows matching on both sides LEFT JOIN All left rows, with NULLs where no match RIGHT JOIN All right rows — rare; rewrite as LEFT FULL OUTER JOIN All rows from both sides SELECT o.id, c.name, o.total FROM orders o JOIN customers c ON c.id = o.customer_id; -- customers with no orders: anti-join idiom SELECT c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL; Filtering a left-joined table in WHERE turns it into an inner join — rows with no match have NULL, and NULL = 'x' is never true. Put that condition in the ON clause instead. Row multiplication One-to-many joins multiply rows. Joining orders to order_items and summing an order-level column will double-count — the most common reporting bug. -- wrong: p.price counted once per item row SELECT SUM(p.price) FROM products p JOIN items i ON i.product_id = p.id; -- right: aggregate each level separately SELECT SUM(t.total) FROM ( SELECT order_id, SUM(quantity * unit_price) AS total FROM items GROUP BY order_id ) t; Self joins and CTEs -- employees and their managers, same table SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON m.id = e.manager_id; -- clearer for multi-step logic WITH monthly AS ( SELECT DATE_TRUNC('month', created_at) AS m, SUM(total) AS revenue FROM orders GROUP BY 1 ) SELECT m, revenue FROM monthly ORDER BY m;"},{"title":"Aggregation and GROUP BY","path":"/learn/sql-group-by/","kind":"Learn","text":"Aggregation and GROUP BY COUNT, SUM, AVG and friends; HAVING versus WHERE; and why COUNT(*) differs from COUNT(col). Aggregate functions COUNT(*) Number of rows — includes NULLs COUNT(col) Non-NULL values only COUNT(DISTINCT col) Unique non-NULL values SUM(col)/AVG(col) NULLs ignored in the maths MIN/MAX Extremes; work on dates and text too STRING_AGG(col, ', ') Concatenate grouped values AVG(col) averages over non-NULL rows only, so it silently ignores missing data rather than treating it as zero. Decide which behaviour you actually want. Grouping SELECT category, COUNT(*) AS n, AVG(price) AS avg_price FROM products WHERE active = TRUE GROUP BY category HAVING COUNT(*) > 5 ORDER BY n DESC; Every non-aggregated column in SELECT must appear in GROUP BY. WHERE filters rows before grouping; HAVING filters the resulting groups. Grouping by an expression? Repeat it in both clauses, or use a CTE with an alias. Window functions (a glimpse) When you need aggregates but want to keep every underlying row, window functions are the answer — no grouping collapse required. SELECT name, category, price, AVG(price) OVER (PARTITION BY category) AS cat_avg, RANK() OVER (ORDER BY price DESC) AS price_rank FROM products;"},{"title":"INSERT, UPDATE, DELETE","path":"/learn/sql-write/","kind":"Learn","text":"INSERT, UPDATE, DELETE Changing data safely: returning clauses, transactions, upserts, and the missing WHERE clause everybody learns once. Inserting INSERT INTO products (name, price, category) VALUES ('Desk lamp', 39.90, 'home'); -- several rows at once INSERT INTO products (name, price) VALUES ('Cable', 9.9), ('Adapter', 14.5); -- copy from another table INSERT INTO archive (id, name) SELECT id, name FROM products WHERE discontinued = TRUE; -- get generated keys back (PostgreSQL) INSERT INTO products (name) VALUES ('Mat') RETURNING id, name; Always list the target columns explicitly. Without them your statement breaks the moment a column is added, reordered, or dropped. Updating and deleting UPDATE products SET price = price * 0.9, updated_at = NOW() WHERE category = 'home' RETURNING id, price; DELETE FROM products WHERE discontinued = TRUE AND stock = 0; A missing WHERE applies the statement to every row. Habits that help: write the WHERE first, wrap ad-hoc changes in a transaction you can roll back, and preview with SELECT using the same predicate. Upserts -- PostgreSQL: insert, or update on conflict INSERT INTO settings (user_id, theme) VALUES (1, 'dark') ON CONFLICT (user_id) DO UPDATE SET theme = EXCLUDED.theme, updated_at = NOW(); -- MySQL equivalent INSERT INTO settings (user_id, theme) VALUES (1, 'dark') ON DUPLICATE KEY UPDATE theme = VALUES(theme); Upserts need a unique constraint or index to detect the conflict — without one nothing conflicts, and you simply duplicate rows. Transactions BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- or ROLLBACK to undo everything A transaction groups statements so they either all succeed or none do — essential whenever two rows must stay consistent (money transfers, inventory)."},{"title":"Indexes and query speed","path":"/learn/sql-indexes/","kind":"Learn","text":"Indexes and query speed How indexes actually help, composite column order, and reading a query plan to find the real bottleneck. What an index gives you An index is a sorted copy of a few columns with pointers to rows — like a book index. It turns a full-table scan into a targeted lookup for selective queries. CREATE INDEX idx_products_category ON products (category); CREATE INDEX idx_orders_customer_created ON orders (customer_id, created_at DESC); CREATE UNIQUE INDEX idx_users_email ON users (email); DROP INDEX idx_products_category; Indexes speed reads but cost writes — every insert or update maintains them. Most planners ignore an index when the predicate returns a large share of rows. Small tables are faster to scan than to index. Column order matters A composite index on (customer_id, created_at) also serves queries filtering only on customer_id — but not ones filtering only on created_at . This left-prefix rule drives most index design. WHERE customer_id = 5 Yes WHERE customer_id = 5 AND created_at > x Yes — both columns WHERE created_at > x No — needs its own index Reading a plan EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42; Seq Scan Full table read — fine for tiny tables, a red flag for big ones Index Scan Index used, then rows fetched Index Only Scan Everything came from the index — fastest high cost / actual time Where the query really spends time Wrap columns in functions and indexes stop working: WHERE DATE(created_at) = '2026-01-01' cannot seek. Use a range instead: created_at >= x AND created_at < x + 1 day . Practical habits Index foreign keys — databases rarely do it for you automatically. Prefer equality-first index column order, then sort/range columns. Avoid leading wildcards: LIKE '%term' cannot use a b-tree index. Measure with EXPLAIN ANALYZE rather than guessing."},{"title":"Git basics","path":"/learn/git-basics/","kind":"Learn","text":"Git basics The three areas every file lives in, the daily commit loop, and writing history worth reading later. Three areas, one mental model Git moves content through the working tree (your files), the index (staged snapshot), and the repository (committed history). Almost every confusing command becomes obvious once you know which two areas it moves files between. git init # create a repository here git status # always the first question git add index.html # working tree -> index git commit -m 'Add landing page' git log --oneline --graph --decorate git add working tree → index git commit index → repository git restore --staged f index → working tree (unstage) git checkout HEAD -- f repository → index + working tree Commits that read well One logical change per commit — not 'stuff from today'. Write the subject in the imperative: 'Add retry logic', not 'Added' or 'Adds'. Explain why in the body; the diff already shows what changed. Keep the subject under about 50 characters so logs stay scannable. git commit -m 'Cap retries at three attempts Upstream timeouts occasionally last 30s; unbounded retries kept workers busy during incidents.' Ignoring files # .gitignore node_modules/ .env *.log .DS_Store # stop tracking a file that is already committed git rm --cached config.local.json Adding a path to .gitignore does not remove it from history — and a committed secret must be treated as leaked regardless. Rotate it, don't just ignore it. Reading differences git diff # unstaged changes git diff --staged # what the next commit contains git show # one commit git diff HEAD~1 HEAD # compare with previous commit git diff --stat # summary instead of full text"},{"title":"Branching and merging","path":"/learn/git-branches/","kind":"Learn","text":"Branching and merging Creating branches, switching safely, merging versus rebasing, and keeping a messy history out of main. Branches are cheap A branch is just a movable pointer to a commit — creating one costs almost nothing, which is why branching per task is the normal workflow. git branch feature/search git switch feature/search # modern equivalent of checkout # or create and switch in one step git switch -c feature/search git branch -a # list local + remote branches git switch - # back to previous branch Prefer git switch and git restore over git checkout . Checkout does too many unrelated things; the newer commands each have one job. Merging git switch main git merge feature/search # creates a merge commit git merge --ff-only feature/x # refuse unless it can fast-forward git branch -d feature/search # delete once merged git log --oneline --graph Fast-forward Main has not moved — history stays linear Merge commit Both branches moved; preserves the true branching history Squash merge One clean commit for a messy feature branch Rebase vs merge # replay your branch onto the latest main git switch feature/search git rebase main # tidy up the last three commits interactively git rebase -i HEAD~3 Rewriting creates new commits with new IDs — accurate history versus honest history is a team decision, but the rule below is not. Never rebase commits that others may already have pulled. Rewriting shared history makes teammates' branches diverge from the remote and leads to duplicated commits. Use merge there."},{"title":"Remotes and collaboration","path":"/learn/git-remotes/","kind":"Learn","text":"Remotes and collaboration Cloning, pushing, fetching versus pulling, and understanding what origin/main actually points at. Connecting git clone git@github.com:user/repo.git cd repo git remote -v git remote add upstream git remote set-url origin origin/main Last known position of the remote's main branch main Your local branch HEAD Current commit you are looking at @{u} Configured upstream tracking branch fetch vs pull git fetch origin # download objects, change nothing local git diff main origin/main # now you can inspect before integrating git merge origin/main # integrate deliberately git pull --rebase # fetch + replay your commits on top git pull is simply fetch followed by merge . Fetching first and inspecting is the safer habit, especially on shared branches. Pushing git push -u origin feature/search # first time: set upstream git push # afterwards git push --force-with-lease # safer than --force git push origin --delete old-branch Prefer --force-with-lease over --force : it refuses to push if someone else has pushed commits you never fetched, rather than silently discarding them. Keeping branches tidy git branch -u origin/main # set tracking git fetch --prune # drop stale remote refs git branch -vv # see tracking relationships"},{"title":"Undoing mistakes","path":"/learn/git-undo/","kind":"Learn","text":"Undoing mistakes restore, reset and revert — and how git reflog rescues commits you thought were gone forever. Pick the right tool Discard uncommitted edits to a file git restore file Unstage a file (keep edits) git restore --staged file Undo last commit, keep changes git reset --soft HEAD~1 Undo last commit, discard changes git reset --hard HEAD~1 Undo a pushed commit safely git revert <sha> Remove untracked files git clean -fd reset --hard permanently discards working-tree changes. Anything uncommitted there is unrecoverable — check git status first. Revert keeps history honest revert does not erase anything: it adds a new commit that applies the inverse change. That is exactly why it is the correct way to undo work that is already public. git revert # single commit git revert .. # range (not including old) git revert -m 1 # specify parent for a merge commit reflog: the safety net Git records every position HEAD has held. For a while after a mistake — a bad reset, a deleted branch — your commits still exist and can be recovered. git reflog git reset --hard HEAD@{3} # return to that state git checkout -b recovered # rescue onto a new branch Reflog is local and expires (90 days for reachable entries by default). It is a recovery tool, never a backup strategy — push important work."},{"title":"Conflicts, stash and cherry-pick","path":"/learn/git-conflicts/","kind":"Learn","text":"Conflicts, stash and cherry-pick Reading conflict markers, resolving without losing work, shelving changes mid-task, and copying single commits. What a conflict actually is Git merges line by line. A conflict means both sides changed overlapping lines and Git will not silently choose — which is the entire point. <<<<<<< HEAD const limit = 20; ======= const limit = 50; >>>>>>> feature/paging Everything above ======= is your current branch; everything below comes from the branch being merged. Delete the markers and keep the correct result (or a genuine combination). git status # shows which files are unmerged git checkout --ours f # keep your side wholesale git checkout --theirs f # keep their side wholesale git add f # mark resolved git merge --continue git merge --abort # give up, return to pre-merge state Using --ours / --theirs for an entire file throws away the other side's work in that file. Prefer hand-editing unless you are certain. Stashing work in progress git stash push -m 'half-done refactor' git stash list git stash pop # restore and drop git stash apply # restore but keep the entry git stash -u # include untracked files Useful for switching branches with uncommitted work. Avoid long-lived stashes — they are easy to forget and painful to merge later. Cherry-pick and bisect git cherry-pick # copy one commit onto this branch # find the commit that introduced a bug git bisect start git bisect bad git bisect good v1.4.0 # test, then mark each step good/bad git bisect reset Cherry-pick copies changes but creates a new commit — fine for hotfixes across release branches, a maintenance burden if used as routine workflow."},{"title":"Shell basics and navigation","path":"/learn/linux-shell-basics/","kind":"Learn","text":"Shell basics and navigation Paths, the commands used every day, and how to read a command line rather than memorizing flags. Paths . Current directory .. Parent directory ~ Your home directory / Root of the filesystem (also a separator) - Previous directory (with cd -) pwd # print working directory ls -la # long list, including hidden files cd /var/log cd ~/projects cd - # tab completion prevents typos cat /etc/ho Anything starting with / is absolute; anything else resolves relative to where you are. That single rule explains most 'file not found' surprises. The daily dozen ls List directory contents cd Change directory cp / mv / rm Copy / move / delete mkdir / touch Create directory / empty file cat / less Dump / page through a file grep Search text head / tail First / last lines man / --help Documentation mkdir -p src/components # -p creates parents, no error if exists touch notes.txt cp file.txt backup.txt cp -r src/ src.bak/ mv old.txt new.txt rm -i important.txt # ask before deleting rm -rf deletes recursively without asking and there is no recycle bin. Pause before running it, especially with globs or variables: rm -rf $DIR/* with an empty DIR is a classic disaster. Wildcards and quoting ls *.js # zero or more characters ls file?.txt # exactly one character cp src/*.js dist/ grep 'error 500' app.log # quote anything with spaces grep \"$PATTERN\" app.log # expand the variable grep '$100' prices.txt # single quotes keep the $ literal The shell expands globs before the command runs, and expands variables inside double quotes but not single quotes. Get those two facts right and most quoting bugs disappear. Saving keystrokes history | grep ssh !! # repeat last command sudo !! # repeat it with sudo Ctrl-R # reverse search through history alias ll='ls -la'"},{"title":"Files and permissions","path":"/learn/linux-files/","kind":"Learn","text":"Files and permissions Reading rwx bits, chmod usage without magic numbers, ownership, and finding files quickly. Reading ls -l -rwxr-xr-- 1 ada dev 4096 Sep 17 10:00 deploy.sh │└┬┘└┬┘└┬┘ │ │ │ └── others: r-- (read only) │ │ └───── group: r-x (read + execute) │ └──────── owner: rwx (full) └────────── file type: - file, d directory, l symlink Permissions are evaluated in order: owner, then group, then others. The first category you match decides what you get — you do not accumulate permissions. Changing permissions chmod +x deploy.sh # add execute for everyone chmod u+x,g-w file # symbolic: user +x, group -w chmod 755 script.sh # numeric: owner 7, group 5, others 5 chmod 600 id_rsa # private key: owner read/write only chmod -R 644 public/ # recursive (careful!) 7 rwx Read, write, execute 6 rw- Read and write 5 r-x Read and execute 4 r-- Read only 0 --- No access chmod -R 777 is never the fix — it hands write access to every account on the machine. Find the right owner or group instead, and remember directories need the execute bit to be entered. Ownership chown ada:dev file.txt # owner:group chown -R www-data:www-data /srv/app id # who am I, which groups groups usermod -aG docker ada # append to a group (needs logout) Finding things find . -name '*.log' -mtime +7 # older than 7 days find /var -type f -size +100M find . -name '*.tmp' -delete # test without -delete first! locate nginx.conf # uses a cached index which node # resolved path of a command Run find without -delete / -exec first and eyeball the list — it is easy to match far more than intended."},{"title":"Working with text","path":"/learn/linux-text/","kind":"Learn","text":"Working with text grep, less, head/tail, pipes and redirection — composing small tools into exactly the answer you need. Looking at files less app.log # space to page, / to search, q to quit head -20 app.log tail -50 app.log tail -f app.log # follow - watch new lines arrive nl file.txt # numbered lines wc -l file.txt # count lines Learn less before anything else: it never loads a whole huge file into memory, unlike opening it in an editor. Searching with grep grep 'ERROR' app.log grep -i 'error' app.log # case-insensitive grep -n 'timeout' app.log # include line numbers grep -r 'TODO' src/ # recursive grep -v 'DEBUG' app.log # invert: lines NOT matching grep -E '4[0-9]{2}|5[0-9]{2}' access.log # extended regex grep -c 'ERROR' app.log # count matching lines -i Ignore case -n Show line numbers -r Recursive into directories -v Invert match -E Extended regular expressions -A/-B/-C n Show after/before/around context Pipes and redirection cat app.log | grep ERROR | wc -l # count errors ps aux | grep node | grep -v grep history | awk '{print $2}' | sort | uniq -c | sort -rn | head grep ERROR app.log > errors.txt # overwrite grep ERROR app.log >> errors.txt # append command 2> errors.txt # stderr only command > out.txt 2>&1 # both together | sends one program's output into the next. File descriptor 1 is stdout, 2 is stderr — that is why 2>&1 means 'stderr, to wherever stdout is going'. Reshaping output cut -d',' -f1,3 data.csv # select fields sort -t',' -k2 -n data.csv # numeric sort on field 2 uniq -c # count adjacent duplicates (sort first!) tr 'a-z' 'A-Z' # translate characters sed -n '10,20p' file # print lines 10-20 awk -F',' '{ sum += $2 } END { print sum }' data.csv uniq only collapses adjacent duplicates — always sort first. This trips up almost everyone once."},{"title":"Processes, ports and jobs","path":"/learn/linux-processes/","kind":"Learn","text":"Processes, ports and jobs Inspecting what runs, stopping it gracefully, checking which port is busy, and keeping work alive after logout. What is running ps aux | grep nginx top # live overview, q to quit htop # friendlier top (install if missing) pgrep -l node pstree -p | head # parent/child relationships USER Owner of the process %CPU / %MEM Resource usage PID Process id — how you target it STAT State; useful when diagnosing zombies Stopping processes kill 1234 # SIGTERM - polite request to stop kill -9 1234 # SIGKILL - cannot be caught, use last pkill -f 'node app' killall nginx Always try SIGTERM before -9 . Killing outright skips cleanup, which can leave locks, partial writes, or orphaned children behind. Ports and connections ss -ltnp # listening TCP ports + process lsof -i :3000 # who owns port 3000 ss -tuln curl -I localhost:3000 # quick health check curl -s -o /dev/null -w '%{http_code} %{time_total}s\\n' https://example.com 'Address already in use' means something holds the port. Find it with lsof -i :PORT rather than blindly restarting. Background jobs long-task & # run in background Ctrl-Z # suspend the foreground job bg # resume it in the background fg # bring it back to foreground jobs # list nohup ./serve.sh > serve.log 2>&1 & # survives logout # or use tmux/screen for a real session Background jobs started with & still belong to your shell; nohup ignores hangup signals. For long-lived work, tmux is more robust than either."},{"title":"SSH, packages and services","path":"/learn/linux-ssh/","kind":"Learn","text":"SSH, packages and services Key-based login without passwords, copying files safely, installing software, and managing services. Connecting ssh ada@server.example ssh -p 2222 ada@server.example ssh -i ~/.ssh/id_ed25519 ada@server # keys beat passwords: immune to brute force, better automation ssh-keygen -t ed25519 -C 'ada@example' ssh-copy-id ada@server ~/.ssh/id_ed25519 Private key — never leaves your machine ~/.ssh/id_ed25519.pub Public key — safe to distribute ~/.ssh/known_hosts Fingerprints of hosts you have seen ~/.ssh/config Per-host shortcuts # ~/.ssh/config - then just: ssh web Host web HostName 203.0.113.10 User ada Port 2222 IdentityFile ~/.ssh/id_ed25519 A private key with loose permissions is refused by SSH: chmod 600 ~/.ssh/id_ed25519 . And never copy a private key onto a shared server — use agent forwarding or generate keys where they are used. Copying files scp file.txt ada@web:/var/www/ scp -r ./dist ada@web:/var/www/app/ scp ada@web:/var/log/app.log ./ # download # rsync: only transfers differences, resumable rsync -avz --progress ./dist/ ada@web:/var/www/app/ rsync -avz --exclude 'node_modules/' ./ ada@web:/srv/app/ Installing software Debian / Ubuntu apt update && apt install nginx RHEL / Fedora dnf install nginx Alpine apk add nginx macOS (Homebrew) brew install node apt search nginx apt show nginx sudo apt update && sudo apt upgrade -y sudo apt autoremove # remove orphaned dependencies Managing services sudo systemctl status nginx sudo systemctl restart nginx sudo systemctl enable nginx # start on boot journalctl -u nginx -f # live logs journalctl -u nginx --since '10 min ago' start begins now, enable configures boot. Most people want both: systemctl enable --now nginx ."},{"title":"HTTP methods","path":"/learn/http-methods/","kind":"Learn","text":"HTTP methods GET, POST, PUT, PATCH and DELETE — what each promises about safety, idempotency, and when to use it. The main methods GET Read a resource No Yes POST Create / trigger an action Yes No PUT Replace a resource wholesale Yes Yes PATCH Partial update Yes No (usually) DELETE Remove a resource Optional Yes HEAD GET without a body (headers only) No Yes OPTIONS Ask what is allowed (used by CORS preflight) No Yes Idempotent means repeating it has the same effect as doing it once — sending the same DELETE twice should leave things identical. That is what makes retries safe. PUT vs PATCH vs POST PUT /users/42 { \"name\": \"Ada\", \"email\": \"ada@example.com\", \"role\": \"admin\" } // replaces the whole record - omitted fields may be cleared PATCH /users/42 { \"role\": \"admin\" } // modifies only the fields provided POST /users { \"name\": \"Ada\" } // creates something new; server assigns the id PUT must be treated as a full replacement. If you only send one field to a PUT endpoint that expects the whole entity, a correct server will blank the rest — that is the contract, not a bug. Practical rules GET and HEAD must never change server state — they get cached, prefetched, and retried. Never put sensitive data in a URL: it lands in logs, Referer headers, and browser history. For failures during creation, idempotency keys let clients retry safely without duplicates. Return Location with 201 Created pointing at the new resource."},{"title":"HTTP status codes","path":"/learn/http-status-codes/","kind":"Learn","text":"HTTP status codes The codes that actually matter in production — and the ones applications routinely get wrong. The five classes 1xx Informational — keep going 2xx Success 3xx Redirection — go elsewhere 4xx Client error — fix the request 5xx Server error — we broke something The ones you will meet daily 200 OK Standard success with a body 201 Created Resource created — send Location 204 No Content Success, nothing to return 301 / 308 Permanent redirect (308 preserves the method) 302 / 307 Temporary redirect (307 preserves the method) 304 Not Modified Cache is still valid — for conditional requests 400 Bad Request Malformed request; failed validation 401 Unauthorized Authentication required or invalid 403 Forbidden Authenticated but not permitted 404 Not Found No such resource 409 Conflict State conflict (duplicate, wrong version) 422 Well-formed but semantically invalid 429 Too Many Requests Rate limited — include Retry-After 500 Unhandled server fault 502 / 503 / 504 Bad gateway / unavailable / gateway timeout Common mistakes Returning 200 with an error body: clients cannot tell success from failure without parsing. Using 401 where 403 belongs: 401 means 'who are you?', 403 means 'I know who you are, and no'. Returning 404 for resources the user may not know exist — sometimes 403/404 by choice for privacy. Redirecting POST with 302 — many clients convert it to GET. Use 307/308 to preserve the method. Do not return 3xx from an API endpoint expecting JSON unless redirect following is guaranteed. Many clients will not follow, and the redirect body gets ignored."},{"title":"HTTP headers","path":"/learn/http-headers/","kind":"Learn","text":"HTTP headers Content negotiation, auth, compression and security headers — the metadata that controls real behaviour. Important request headers Host Virtual host routing — mandatory in HTTP/1.1 Accept Response formats the client can handle Content-Type Media type of the body being sent Authorization Credentials (Bearer token, Basic) If-None-Match Conditional request using an ETag User-Agent Client identification Referer Previous page (spelled wrong in the original spec, forever) Important response headers Content-Type What the body is — including charset Content-Length/Transfer-Encoding Body size or chunking Cache-Control Freshness rules ETag Version fingerprint for conditional requests Set-Cookie Store a cookie (may appear several times) Location Target for 3xx and 201 responses Vary Which request headers affect the response Vary is easy to forget and expensive to get wrong: any cache key difference must be declared there, or one user's content can be served to another. Security headers worth setting Strict-Transport-Security: max-age=31536000; includeSubDomains Content-Security-Policy: default-src 'self' X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: geolocation=(), camera=() X-Frame-Options: DENY (or CSP frame-ancestors) HSTS is cached by browsers for its whole max-age . Start with a short value (5 minutes) and increase it once you are certain HTTPS works everywhere — otherwise you can lock yourself out."},{"title":"Caching and conditional requests","path":"/learn/http-caching/","kind":"Learn","text":"Caching and conditional requests Cache-Control in plain language, ETags versus Last-Modified, and revalidation with 304 responses. Controlling freshness Cache-Control: max-age=3600, public Cache-Control: no-store # never keep it (sensitive data) Cache-Control: no-cache # store, but revalidate every time Cache-Control: private, max-age=600 Cache-Control: immutable, max-age=31536000 # fingerprinted assets max-age=n Fresh for n seconds s-maxage=n Same, for shared caches (CDN) only public/private Whether proxies may cache it no-cache Cache it, but always revalidate first no-store Do not store at all stale-while-revalidate=n Serve stale, refresh in the background no-cache does not mean 'do not cache' — that is no-store . The naming has confused people for decades. Conditional requests Once a response goes stale, the client asks 'has this changed?' rather than downloading it again. Confirmation costs a tiny 304 with no body — the single biggest bandwidth saving available. # first response HTTP/1.1 200 OK ETag: \"a1b2c3\" Cache-Control: max-age=0, must-revalidate # later request GET /api/profile HTTP/1.1 If-None-Match: \"a1b2c3\" # unchanged response - no body transmitted HTTP/1.1 304 Not Modified ETag / If-None-Match Version fingerprint — precise Last-Modified / If-Modified-Since Timestamp — one-second resolution A pragmatic setup Hashed asset filenames (app.a1b2c3.js) can be cached for a year with immutable. HTML should be no-cache or short-lived so deploys take effect immediately. The CDN pattern: Cache-Control: public, max-age=0, s-maxage=86400, stale-while-revalidate=60. Never cache personalized responses (Cache-Control: private) in a shared CDN."},{"title":"CORS explained","path":"/learn/http-cors/","kind":"Learn","text":"CORS explained Why the browser blocks cross-origin reads, how preflight works, and the headers that fix it properly. The problem CORS solves Your browser attaches cookies and tokens to requests automatically. Without protection, any site you visited could read data from another service you are logged into — your bank, your mail. The same-origin policy blocks reading cross-origin responses; CORS is how a server explicitly relaxes that. CORS is a browser protection, not a security mechanism for your server. It constrains browsers — anything else (curl, scripts, servers) ignores it entirely. Authorization must still be enforced server-side. Simple requests vs preflight A request using only GET/HEAD/POST with simple headers is sent directly. Anything else — custom headers, PUT/PATCH/DELETE, JSON bodies — triggers a preflight OPTIONS request asking permission first. OPTIONS /api/item HTTP/1.1 Origin: https://app.example Access-Control-Request-Method: DELETE Access-Control-Request-Headers: authorization HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example Access-Control-Allow-Methods: GET, POST, DELETE Access-Control-Allow-Headers: authorization Access-Control-Max-Age: 600 The response headers Access-Control-Allow-Origin Permitted origin, or * (never with credentials) Access-Control-Allow-Credentials Allow cookies — requires an explicit origin Access-Control-Allow-Methods Allowed methods for preflight Access-Control-Allow-Headers Allowed request headers Access-Control-Max-Age Cache preflight result (seconds) Vary: Origin Essential when the origin varies per caller Access-Control-Allow-Origin: * combined with Allow-Credentials: true is rejected by browsers — a wildcard cannot authorise credentials. Echo the specific origin instead, and always send Vary: Origin . Fixing it in practice A CORS error is always fixed on the server that owns the resource . No amount of fetch options or browser flags will bypass it. // Express: allow one trusted app app.use(cors({ origin: 'https://app.example', credentials: true })); // fetch side: include cookies when needed fetch(url, { credentials: 'include' }); mode: 'no-cors' does not fix anything — it gives you an opaque response you cannot read. During development, proxy through your dev server instead of opening your API to every origin. A reverse proxy serving API and app on one origin removes CORS questions altogether."},{"title":"HTTPS and TLS","path":"/learn/http-https-tls/","kind":"Learn","text":"HTTPS and TLS What TLS actually protects, how certificates get validated, renewal automation, and mixed content traps. What HTTPS gives you Confidentiality Nobody on the network can read the traffic Integrity Tampering is detected, not silently accepted Authentication You are talking to the real server, not an impostor Note what it does not do: HTTPS does not make a site trustworthy, and it encrypts only the transport — the server still sees everything once decrypted. Certificate chains A browser trusts a certificate because it chains to a root already in its trust store. You normally install a leaf certificate plus any intermediate certificates — a missing intermediate is the most common 'works in some browsers' failure. openssl s_client -connect example.com:443 -servername example.com echo | openssl s_client -connect example.com:443 2>/dev/null \\ | openssl x509 -noout -dates -subject -issuer Domain-validated (DV) certificates are free and automated — enough for most sites. Wildcard certs cover *.example.com but never the bare domain; include both. Certificate transparency logs are public — treat certificate issuance as observable globally. Renewal without drama # Let's Encrypt with Certbot certbot certonly --webroot -w /var/www/app -d example.com -d www.example.com certbot renew --dry-run # certificates typically last 90 days; automate renewal systemctl list-timers | grep certbot Short-lived certificates (90 days or less) are the modern norm. Automate renewal and set expiry monitoring — certificate expiry is still a leading cause of avoidable outages. Mixed content Loading any subresource over http:// in an https:// page weakens the guarantee. Modern browsers silently upgrade images but block scripts and styles — showing 'unexpected behaviour' rather than an obvious error. Add Content-Security-Policy: upgrade-insecure-requests to rewrite accidental http URLs automatically, and always send HSTS once you are confident."},{"title":"Base64 encoding","path":"/learn/base64/","kind":"Learn","text":"Base64 encoding What Base64 is, why it exists, how it works, and the cases where it is the wrong tool. What Base64 is Base64 is a way to represent arbitrary binary data using only 64 printable ASCII characters (A–Z, a–z, 0–9, + and / , with = for padding). It is an encoding , not encryption: anyone can decode it, and it provides no confidentiality or integrity. It was designed for a world where data had to travel through systems built for plain text — email (SMTP), early Usenet, and various configuration formats — that might mangle control characters or 8-bit bytes. Man → TWFu M a n 01001101 01100001 01101110 (3 bytes / 24 bits) |||||||| |||||||| |||||||| T W F u (split into four 6-bit groups → 4 Base64 chars) How the encoding works Base64 groups the input into 3-byte (24-bit) chunks, then splits each chunk into four 6-bit values. Each 6-bit value (0–63) maps to one character in the alphabet. If the final chunk has only 1 or 2 bytes, it is zero-padded and the output is padded with one or two = signs. 3 input bytes → exactly 4 output characters (no padding). 2 input bytes → 3 characters + one = pad. 1 input byte → 2 characters + two = pads. Base64 makes data about 33% larger (4 chars per 3 bytes). Never use it to save space — that is what compression (gzip, zstd) is for. Where you actually see it Data URLs in CSS/HTML: data:image/png;base64,iVBORw0… embeds a small image inline. HTTP: Basic auth sends Authorization: Basic base64(user:pass) (over TLS only!). JSON / APIs: binary blobs (files, keys) are often Base64-encoded before being placed in a text field. Certificates & keys: PEM files are Base64-wrapped DER with -----BEGIN…----- headers. Email attachments: MIME Content-Transfer-Encoding: base64. Base64 is not a security control . Encoding a password in Base64 is the same as writing it down in a different font. Use a password hash (see Hashing & checksums ) for secrets. Common variants Standard Alphabet +/, pad =. URL-safe Uses -_ instead of +/ so it is safe in URLs/paths (no padding issues). Base32 / Base58 Used where case-insensitivity or human-typing matters (e.g. Bitcoin addresses use Base58)."},{"title":"URL encoding (percent-encoding)","path":"/learn/url-encoding/","kind":"Learn","text":"URL encoding (percent-encoding) Why spaces and symbols in a URL get turned into %20 and friends, and the difference between query and path encoding. What percent-encoding is URLs may only contain a limited set of characters from the ASCII set. Any character outside that set — or a reserved character used for its literal value — is encoded as a % followed by two uppercase hex digits representing its byte. hello world → hello%20world c++ & c# → c%2B%2B%20%26%20c%23 price=€10 → price%3D%E2%82%AC10 (€ is 3 UTF-8 bytes: E2 82 AC) Why it exists The URL grammar reserves characters such as ? , & , = , # , / for structure. If a value legitimately contains one of them, the parser must know it is data, not syntax. Encoding disambiguates. A space may be encoded as %20 almost everywhere, but in the application/x-www-form-urlencoded body used by HTML forms a space becomes a + . That is a different rule for a different context. Encoding is over bytes, not characters Modern URLs encode the UTF-8 bytes of the character, not the code point directly. é (U+00E9) is one UTF-8 byte 0xC3 0xA9 , so it becomes %C3%A9 . This is why the same character always encodes the same way regardless of the platform. // JavaScript const s = 'café'; const enc = encodeURIComponent(s); // \"caf%C3%A9\" const dec = decodeURIComponent(enc); // \"café\""},{"title":"UTF-8 and character sets","path":"/learn/utf8/","kind":"Learn","text":"UTF-8 and character sets Why 'one character = one byte' is wrong, what code points are, and how UTF-8 became the default for the web. Characters are not bytes A code point is a number assigned to a character by the Unicode standard (for example, A is U+0041, é is U+00E9, 😀 is U+1F600). An encoding decides how that number is stored as bytes. The same character can be stored differently by UTF-8, UTF-16, or UTF-32. Mixing encodings is the classic cause of mojibake (garbled text like é ). Always declare and agree on one encoding end to end. How UTF-8 works UTF-8 is a variable-width encoding: ASCII characters (0–127) take exactly one byte and are identical to ASCII, which is why legacy English text keeps working. Other characters take 2–4 bytes. U+0000 – U+007F 1 byte (same as ASCII) U+0080 – U+07FF 2 bytes U+0800 – U+FFFF 3 bytes U+10000 – U+10FFFF 4 bytes A → 0x41 (1 byte) é → 0xC3 0xA9 (2 bytes) 中 → 0xE4 0xB8 0xAD (3 bytes) 😀 → 0xF0 0x9F 0x98 0x80 (4 bytes) UTF-8 is the web default Declare it: <meta charset=\"utf-8\"> in HTML, and Content-Type: text/html; charset=utf-8 over HTTP. For APIs, JSON text is defined by the spec to be UTF-8; do not add a BOM. Read files with an explicit encoding: open(f, encoding='utf-8') in Python, not the platform default. The Byte Order Mark (BOM) is meaningless for UTF-8 and often breaks parsers (JSON, shell scripts). Save UTF-8 without BOM."},{"title":"Hash functions: MD5, SHA-1, SHA-256","path":"/learn/hashing/","kind":"Learn","text":"Hash functions: MD5, SHA-1, SHA-256 What a cryptographic hash is, how the common algorithms differ, and why MD5/SHA-1 are retired for security. What a hash function does A cryptographic hash function takes input of any size and returns a fixed-length fingerprint (the digest ). Good properties: deterministic (same input → same output), fast to compute, and preimage-resistant (you cannot reverse it) and collision-resistant (you cannot find two inputs with the same digest). sha256(\"hello\") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 MD5, SHA-1, SHA-256 compared MD5 128-bit Broken — collisions are trivial; never use for security. SHA-1 160-bit Broken — collision found in 2017; deprecated for signatures. SHA-256 256-bit Current standard (SHA-2 family); safe for integrity & signatures. SHA-3 / BLAKE3 variable Modern alternatives; BLAKE3 is very fast. A hash proves integrity (data unchanged), not authenticity . To verify the sender you need a keyed MAC (HMAC) or a signature. Hashing passwords is different Plain SHA-256 is not enough for passwords: it is too fast, so attackers brute-force it cheaply. Password hashing needs to be slow and salted — use bcrypt , scrypt , Argon2 , or PBKDF2 . # DO NOT: hash = sha256(password) # fast, unsalted # DO: a slow, salted KDF import hashlib, secrets pwd = b'correct horse battery staple' salt = secrets.token_bytes(16) dk = hashlib.pbkdf2_hmac('sha256', pwd, salt, 200000) # store salt + dk; verify by recomputing"},{"title":"Checksums & verifying files","path":"/learn/checksums/","kind":"Learn","text":"Checksums & verifying files How to confirm a downloaded file is intact and unmodified using a checksum, and the limits of the technique. Why verify a file When you download software, a flipped bit or a tampered mirror can corrupt or poison the file. A published checksum lets you confirm the bytes you received match what the publisher intended. # Linux / macOS sha256sum downloaded.iso # Windows (PowerShell) Get-FileHash downloaded.iso -Algorithm SHA256 What a checksum cannot prove A checksum only proves the file matches a known value. If the attacker controls both the file and the published checksum on the same page, verification passes. For supply-chain trust use code signing or reproducible builds. Checksums detect accidental corruption (transmission errors). Signatures (GPG, Sigstore) detect malicious tampering by an unknown party. Prefer SHA-256 over MD5/SHA-1 for new publish checksums."},{"title":"JSON basics","path":"/learn/json/","kind":"Learn","text":"JSON basics The shape of JSON, how it maps to language types, and the easy mistakes that produce invalid JSON. The JSON type system JSON (JavaScript Object Notation) is a text format for structured data built from just two containers — objects {} and arrays [] — and a small set of scalar values: string, number, boolean, null . object Object dict array Array list string String str number Number int / float true/false boolean bool null null None Rules that break parsers Keys MUST be double-quoted strings. No trailing comma after the last element. No comments, no unquoted keys, no unquoted constants other than true/false/null. One value per document at the top level (object or array). JSON is not JavaScript. {a: 1} is valid JS but invalid JSON — the key needs quotes. This is the #1 cause of parse errors. { \"name\": \"Ada\", \"age\": 36, \"skills\": [\"math\", \"logic\"], \"active\": true, \"score\": null } Reading and writing it // Parse (throws on invalid JSON) const obj = JSON.parse(text); // Serialize (replacer + indent for readability) const text = JSON.stringify(obj, null, 2); import json obj = json.loads(text) # parse text = json.dumps(obj, indent=2) # pretty-print"},{"title":"CSV vs JSON","path":"/learn/csv-json/","kind":"Learn","text":"CSV vs JSON When a flat table beats nested documents, and how to convert between them safely. Pick the right shape CSV is a flat grid of rows and columns — perfect for tabular data exported from spreadsheets and databases. JSON expresses nested, heterogeneous structures (objects inside arrays inside objects) that CSV cannot represent without conventions. Data is a simple table Data is hierarchical / nested Humans edit it in Excel An API consumes it One type of record Mixed or optional fields Converting safely CSV has no standard type system — everything is text. Decide explicitly whether \"123\" becomes a number or stays a string, or you will get silent type bugs. import csv, json with open('data.csv', newline='', encoding='utf-8') as f: rows = list(csv.DictReader(f)) with open('data.json', 'w', encoding='utf-8') as f: json.dump(rows, f, indent=2, ensure_ascii=False) Going the other way (JSON → CSV) only works cleanly when every record shares the same flat keys; otherwise you must flatten nested fields into dotted column names."},{"title":"Unix timestamps & UTC","path":"/learn/unix-timestamp/","kind":"Learn","text":"Unix timestamps & UTC Why storing time as a single number avoids almost every timezone bug, and the 2038 problem you should know about. What a Unix timestamp is A Unix timestamp is the number of seconds (or milliseconds) since the epoch : 1970-01-01 00:00:00 UTC. It is timezone-neutral: the same instant has the same timestamp everywhere on Earth. 2026-09-17 12:00:00 UTC → 1787112000 2026-09-17 08:00:00 EDT → 1787112000 (same instant!) 2026-09-17 20:00:00 +08:00 → 1787112000 Store UTC, display local The robust pattern: keep time in UTC (or as a timestamp) internally, and convert to the user's local zone only at the display layer. Never store 'local time' without the zone. const now = Math.floor(Date.now() / 1000); // seconds since epoch const d = new Date(); console.log(d.toISOString()); // always UTC: 2026-09-17T12:00:00.000Z from datetime import datetime, timezone now = datetime.now(timezone.utc) print(int(now.timestamp())) # epoch seconds print(now.isoformat()) # 2026-09-17T12:00:00+00:00 The 2038 problem Systems that store the timestamp as a signed 32-bit integer overflow on 2038-01-19. Modern languages use 64-bit values, but legacy C code and some embedded systems still need migration. If you maintain older C/C++ services, audit time_t usage now — 2038 is closer than it looks."},{"title":"Time zones & UTC offsets","path":"/learn/timezone/","kind":"Learn","text":"Time zones & UTC offsets Why 'UTC+8' lies, what IANA zone names are for, and how to avoid the classic off-by-one date bugs. Offsets are not zones An offset like UTC+8 only says '8 hours ahead of UTC'. It does not tell you which region's rules apply — when daylight saving starts, or when the policy changes. Two places with the same offset today can diverge tomorrow. Always store and transmit IANA zone names (e.g. Asia/Shanghai , America/New_York ), never bare offsets. Zones encode the full history and future of DST rules. Use ISO 8601 with the zone 2026-09-17T12:00:00Z (Z = UTC) 2026-09-17T20:00:00+08:00 (with explicit offset) 2026-09-17T08:00:00-04:00 (same instant, different zone) The trailing Z means UTC. Including the offset (or zone) makes the value unambiguous — essential for logs, APIs, and scheduling. Common date bugs Doing date math in local time across a DST boundary (use UTC, or a tz-aware library). Assuming every day has 24 hours (DST spring-forward has 23). Formatting a UTC instant as a date without converting to the user's zone (off-by-one). Parsing non-ISO strings like '09/17/2026' (is that Sept 17 or 9th of month 17?)."},{"title":"Regular expressions basics","path":"/learn/regex-basics/","kind":"Learn","text":"Regular expressions basics The handful of regex constructs you actually use daily, with runnable examples and the mistakes that bite beginners. The core atoms . any single character (except newline) \\d a digit; \\w a word char; \\s whitespace ^ / $ start / end of string (or line with /m) [abc] one of a, b, or c; [^0-9] negated a* / a+ zero-or-more / one-or-more of a a? optional (zero or one) a{2,4} between 2 and 4 of a Groups and alternation Parentheses (…) capture a part so you can extract or backreference it. The pipe | means 'or'. Escape special characters with a backslash when you mean the literal character. const re = /(\\d{4})-(\\d{2})-(\\d{2})/; // YYYY-MM-DD const m = re.exec('2026-09-17'); console.log(m[1], m[2], m[3]); // 2026 09 17 import re m = re.search(r'(\\d{4})-(\\d{2})-(\\d{2})', '2026-09-17') print(m.groups()) # ('2026', '09', '17') Mistakes that bite Forgetting to escape . — it matches any char, so a.b also matches aXb. Use \\. for a literal dot. Catastrophic backtracking: nested quantifiers like (a+)+$ on hostile input can hang. Prefer possessive/atomic patterns or a parser. Using regex to parse HTML/JSON — use a real parser instead. Assuming \\w covers Unicode letters — in many engines it is ASCII-only; use the Unicode flag. Validate, don't trust. If a regex is used for security (e.g. path or email checks), confirm it actually rejects bad input — and prefer library validators where they exist."},{"title":"Tools","path":"/tools/","kind":"Page","text":"base64 hash json url case converter tools"},{"title":"Base64 encoder / decoder","path":"/tools/base64/","kind":"Tool","text":"Convert text to and from Base64. Runs entirely in your browser — nothing is uploaded. Base64 encoder / decoder"},{"title":"Hash & checksum","path":"/tools/hash/","kind":"Tool","text":"Compute MD5, SHA-1, SHA-256, SHA-384, SHA-512 of text using your browser. No upload. Hash & checksum"},{"title":"JSON formatter & validator","path":"/tools/json/","kind":"Tool","text":"Pretty-print, minify, and validate JSON. Runs locally in your browser. JSON formatter & validator"},{"title":"URL encoder / decoder","path":"/tools/url/","kind":"Tool","text":"Percent-encode and decode query strings and path segments. Browser-only. URL encoder / decoder"},{"title":"Case converter","path":"/tools/case/","kind":"Tool","text":"Convert identifiers between camelCase, snake_case, kebab-case, PascalCase, and CONSTANT_CASE. Case converter"},{"title":"About","path":"/about/","kind":"Page","text":"About HowToCodePage: practical developer tutorials and browser-only tools."},{"title":"Privacy","path":"/privacy/","kind":"Page","text":"HowToCodePage privacy: tools run locally, no data collection."}]