Edward Hernandez

How to Set Up Automated Testing in a React Project: Vitest, React Testing Library, and Playwright

Adding automated testing to a React project is one of those tasks that stays in the backlog for months, mostly because the tooling advice online is fragmented: one article covers Jest, another covers Cypress, and none of them show how the pieces fit together in a real codebase. This tutorial fixes that. In a single afternoon you will bolt a complete, production-grade testing stack onto an existing React app: Vitest for unit tests, React Testing Library for component tests, MSW for API mocking, and Playwright for end-to-end tests, all wired into GitHub Actions with coverage thresholds that actually fail the build. Every snippet below is copy-paste ready. We build two real test suites that mirror what you have in almost every app: a signup form (validation, submission, pending state) and an API-driven list (loading, success, empty, error). The testing stack at a glance Automated testing in a React project works best as layers. Each layer answers a different question, runs at a different speed, and breaks for a different reason. Layer Tool What it verifies Typical speed Share of your suite Static analysis TypeScript + ESLint Types, dead code, bad hooks usage Seconds Always on Unit Vitest Pure functions, reducers, validators, hooks Milliseconds ~50% Component / integration Vitest + React Testing Library + MSW Rendered UI, user interactions, network states Tens of ms ~40% End-to-end Playwright Critical user journeys in a real browser Seconds per test ~10% Why Vitest instead of Jest in 2026 Jest still works and is still everywhere, but if your React app is built with Vite (the default for most new projects since Create React App was deprecated), Vitest is the lower-friction choice. Criterion Vitest Jest Config Reuses your existing vite.config.ts (aliases, env, plugins) Separate config plus a transform (babel-jest, ts-jest or SWC) TypeScript and ESM Native Workable, but ESM still needs care Watch mode speed Very fast, module-graph aware Good, slower cold start API compatibility Jest-compatible (describe, it, expect, mocks) The reference API Browser-mode component tests Built in (real browser, no jsdom) Not available Migrating an existing Jest suite? Most files run unchanged once you replace jest.fn() with vi.fn() and jest.mock() with vi.mock(). Keep Jest if you are on Next.js with a heavy Babel setup and you have no pain today; the rest of this guide still applies almost line for line, since React Testing Library, MSW and Playwright are runner-agnostic. reactjs.org makes the same point with more data. Prerequisites An existing React 19 app scaffolded with Vite (TypeScript recommended) Node.js 22 LTS or newer A package manager: npm, pnpm or yarn (commands below use npm) Roughly 3 hours of uninterrupted time Step 1: Install the unit and component testing layer npm install -D vitest @vitest/coverage-v8 jsdom \ @testing-library/react @testing-library/dom @testing-library/user-event \ @testing-library/jest-dom msw What each package does: vitest: the test runner and assertion library @vitest/coverage-v8: coverage reporting via the V8 engine jsdom: a DOM implementation so components can render in Node @testing-library/react: render components and query them the way a user sees them @testing-library/user-event: realistic typing, clicking, tabbing and pasting @testing-library/jest-dom: DOM matchers such as toBeVisible() and toHaveAccessibleName() msw: intercepts network requests at the HTTP layer instead of mocking fetch Step 2: Configure Vitest Import defineConfig from vitest/config so the test key is typed. Note the exclude entry: without it, Vitest will try to run your Playwright specs and fail. // vite.config.ts import { defineConfig } from ‘vitest/config’ import react from ‘@vitejs/plugin-react’ export default defineConfig({ plugins: [react()], test: { globals: true, environment: ‘jsdom’, setupFiles: ‘./src/test/setup.ts’, css: true, include: [‘src/**/*.{test,spec}.{ts,tsx}’], exclude: [‘**/node_modules/**’, ‘**/dist/**’, ‘**/e2e/**’], clearMocks: true, restoreMocks: true, coverage: { provider: ‘v8’, reporter: [‘text’, ‘html’, ‘lcov’], include: [‘src/**/*.{ts,tsx}’], exclude: [ ‘src/**/*.{test,spec}.{ts,tsx}’, ‘src/test/**’, ‘src/main.tsx’, ‘src/**/*.d.ts’, ], thresholds: { lines: 80, functions: 80, branches: 70, statements: 80, }, }, }, }) The setup file This single file registers the DOM matchers, cleans the DOM between tests, and starts the MSW server for the whole suite. // src/test/setup.ts import ‘@testing-library/jest-dom/vitest’ import { cleanup } from ‘@testing-library/react’ import { afterAll, afterEach, beforeAll } from ‘vitest’ import { server } from ‘./msw/server’ beforeAll(() => server.listen({ onUnhandledRequest: ‘error’ })) afterEach(() => { cleanup() server.resetHandlers() }) afterAll(() => server.close()) Important: onUnhandledRequest: ‘error’ is what turns MSW into a safety net. If a component starts calling an endpoint nobody mocked, the test fails loudly instead of silently hanging. Mock handlers and MSW server // src/test/msw/handlers.ts import { http, HttpResponse } from ‘msw’ export const handlers = [ http.get(‘/api/users’, () => HttpResponse.json([ { id: 1, name: ‘Ada Lovelace’, email: ‘[email protected]’ }, { id: 2, name: ‘Alan Turing’, email: ‘[email protected]’ }, ]), ), http.post(‘/api/signup’, () => HttpResponse.json({ id: 42 }, { status: 201 }), ), ] // src/test/msw/server.ts import { setupServer } from ‘msw/node’ import { handlers } from ‘./handlers’ export const server = setupServer(…handlers) Add the scripts // package.json (excerpt) { “scripts”: { “dev”: “vite”, “build”: “tsc -b && vite build”, “preview”: “vite preview”, “lint”: “eslint .”, “test”: “vitest”, “test:run”: “vitest run”, “test:coverage”: “vitest run –coverage”, “test:e2e”: “playwright test”, “test:e2e:ui”: “playwright test –ui”, “test:all”: “npm run test:run && npm run test:e2e” } } Run npm test now. Vitest should start in watch mode and report that no tests were found. That is your green light. Step 3: Component test number one, the signup form Forms are where most regressions hide: validation rules, disabled buttons, double submits, error announcements. Here is the component under test. // src/components/SignupForm.tsx import { useState, type FormEvent } from ‘react’ type Values = { email: string; password: string } type Props = { onSubmit: (values: Values) => Promise<void> | void } type Errors = { email?: string; password?: string } export function SignupForm({ onSubmit }: Props) { const [email, setEmail] = useState(”) const [password, setPassword] = useState(”) const [errors, setErrors] = useState<Errors>({}) const [pending, setPending] = useState(false) async function handleSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault() const next: Errors = {} const cleanEmail = email.trim() if (!/^\S+@\S+\.\S+$/.test(cleanEmail)) next.email = ‘Enter a valid email address’ if (password.length < 8) next.password = ‘Password must be at least 8 characters’ setErrors(next) if (Object.keys(next).length > 0)

How to Set Up Automated Testing in a React Project: Vitest, React Testing Library, and Playwright Read More »

Best VS Code Extensions for Full Stack Developers: 15 That Actually Save Time

Every full stack developer eventually hits the same wall: the editor that felt instant on day one takes eight seconds to open a project six months later. Usually the code base is not the problem. The extension list is. This is a curated, opinionated review of the best VS Code extensions for full stack developers, based on daily use across React/Next.js front-ends, Node, .NET and Python back-ends, Docker-based environments, SQL and NoSQL databases, and Git workflows. For each one you will find what it actually replaces in your daily routine, because an extension that does not remove a manual step is just startup time you paid for. At the end, the part most listicles skip: the popular extensions that slow the editor down, duplicate features VS Code now ships natively, or are no longer maintained. More at https://blueboxes.co.uk. Quick answer: the 15 extensions worth installing # Extension Layer What it replaces 1 ESLint Front-end / Node Running npm run lint in a terminal loop 2 Prettier All Formatting arguments in pull requests 3 Tailwind CSS IntelliSense Front-end The Tailwind docs tab you keep open 4 Error Lens All Hovering squiggles and opening the Problems panel 5 Live Preview Front-end Alt-tabbing to a browser and pressing F5 6 Python + Pylance Back-end Guessing types and virtualenv juggling 7 C# Dev Kit Back-end Opening Visual Studio just to run tests 8 REST Client API Postman for 90% of requests 9 Path Intellisense All Typing ../../.. and hoping 10 Container Tools (Docker) Infra Memorised docker ps and docker logs commands 11 Dev Containers Infra The “works on my machine” onboarding day 12 SQLTools Database DBeaver or pgAdmin for quick queries 13 MongoDB for VS Code Database Compass and the mongosh shell 14 GitLens Git git blame and archaeology in the browser 15 GitHub Pull Requests Git Reviewing code in 14 browser tabs Now the details, and the honest caveats. Front-end extensions that earn their memory 1. ESLint Still the backbone of any JavaScript or TypeScript stack. With flat config (eslint.config.js) now the default, the extension picks up your rules without extra setup in most projects. Replaces: the terminal lint loop and half of your code review comments. Set this: “editor.codeActionsOnSave”: { “source.fixAll.eslint”: “explicit” }. Cost: low, but on very large monorepos restrict it with eslint.workingDirectories or the language server will chew CPU on every keystroke. 2. Prettier Boring, essential, and the single fastest way to stop arguing about style. The important part is discipline: pick one formatter per language and set it explicitly per file type in your settings so VS Code never has to guess. Replaces: formatting nitpicks in pull requests and manual reindentation. Watch out: if you also installed Beautify, JS-CSS-HTML Formatter or a framework-specific formatter, you will get random reformat wars. Keep one. 3. Tailwind CSS IntelliSense If your project uses Tailwind v4, this is not optional. Class autocompletion, hover previews of the generated CSS, colour swatches and warnings for conflicting classes. Replaces: the permanently open Tailwind documentation tab. Tip: add tailwindCSS.experimental.classRegex so it also completes classes inside cva, clsx or tailwind-merge helpers. 4. Error Lens It prints diagnostics inline at the end of the offending line. Sounds cosmetic, but it changes your feedback loop: you see a TypeScript error the moment it appears instead of after a build. Replaces: hovering over squiggles and scanning the Problems panel. Honest caveat: on files with hundreds of warnings it becomes visual noise. Limit it with errorLens.enabledDiagnosticLevels: [“error”, “warning”] and consider disabling it in legacy folders. 5. Live Preview (Microsoft) An embedded browser preview with hot reload for static HTML/CSS work, maintained by Microsoft. For static sites, landing pages, email templates and quick prototypes, it is lighter than the classic Live Server and it lives inside the editor. Replaces: Live Server plus manual browser refreshes. Note: for Vite, Next.js or Nuxt, you already have hot reload. Use the built-in Simple Browser (Ctrl+Shift+P then “Simple Browser”) and skip this one. Back-end and API extensions 6. Python + Pylance The Microsoft Python extension pack handles interpreter selection, test discovery, debugging and environment detection, while Pylance provides fast type inference. If your back-end is Django, FastAPI or Flask, this is the baseline. Replaces: manual venv activation, pytest command lines and print-statement debugging. Tip: set python.analysis.typeCheckingMode to basic. It catches real bugs without drowning you in red. 7. C# Dev Kit For .NET developers who prefer VS Code over the full Visual Studio, C# Dev Kit brings solution explorer, test explorer and proper project management on top of the C# extension. Replaces: switching to Visual Studio just to run a test suite or add a project reference. Licence check: free for individuals, students and open source, but paid organisations need a Visual Studio subscription. Verify before rolling it out to a team. 8. REST Client Write requests in a plain .http file, hit send, read the response in a split pane. Because the file lives in the repository, your API calls get versioned with the code and reviewed like anything else. ### Create a user POST http://localhost:3000/api/users Content-Type: application/json { “email”: “[email protected]”, “role”: “admin” } Replaces: Postman for most day to day calls, and the collection you forgot to share with the team. Alternative: VS Code also supports .http files natively in recent builds, and Thunder Client works but moved key features behind a paid plan. REST Client stays free. 9. Path Intellisense Autocompletes file paths in imports, src attributes and config files. Tiny, fast, and it prevents the classic broken relative import after a refactor. Replaces: counting ../ segments by hand. Docker and environment extensions 10. Container Tools (formerly Docker) Microsoft’s container extension, renamed from Docker to Container Tools, gives you a sidebar with images, containers, volumes and registries, plus Dockerfile and Compose scaffolding and one-click log tailing. Replaces: the docker ps, docker logs -f, docker exec -it muscle memory. Pair it with: the Docker DX extension for Dockerfile and Compose linting if you write a lot of container config. 11. Dev Containers The one

Best VS Code Extensions for Full Stack Developers: 15 That Actually Save Time Read More »

WCAG Accessibility Checklist for Developers: 22 Checks to Run Before You Ship

Most WCAG resources are written for auditors and compliance officers. They list 50+ success criteria, explain intent, and leave you guessing about what to actually click. This WCAG accessibility checklist is the opposite: 22 concrete, pass/fail checks a developer can run on a single page or template in under an hour, each mapped to the exact WCAG 2.2 Level A/AA criterion it satisfies, with the browser and screen reader tool to use for it. As of August 2026, WCAG 2.2 is the current W3C Recommendation (WCAG 3.0 is still an early working draft, so do not plan releases around it). WCAG 2.2 adds nine success criteria to 2.1 and removes 4.1.1 Parsing, which is now obsolete. Everything below reflects 2.2. How to use this checklist Scope it to one page or template at a time. A homepage, a checkout step, a data table view, a modal-heavy dashboard screen. Auditing “the whole site” in one pass never works. Target Level AA. That is the level referenced by the European Accessibility Act / EN 301 549, the ADA Title II web rule, Section 508 and most enterprise procurement contracts. Record a verdict, not a feeling. Each check is Pass, Fail or N/A. Every Fail gets a ticket with the criterion number in the title, for example “[a11y][2.4.11] Focus hidden behind sticky header on /pricing”. Do not stop at the automated scan. Automated tooling reliably detects roughly a third of WCAG issues. Checks 1 to 7 and 18 to 22 below are mostly manual for a reason. Tools to install before you start Tool What you use it for axe DevTools or WAVE (browser extension) First automated pass, contrast and ARIA errors Accessibility Insights for Web Tab stop visualisation, guided manual tests Chrome/Edge DevTools Accessibility tree pane, contrast picker, emulation (reduced motion, forced colors) TPGi Colour Contrast Analyser Contrast of gradients, images, canvas, video overlays NVDA + Firefox (Windows) or VoiceOver + Safari (macOS) Names, roles, states, live region announcements axe-core in CI (Playwright, Cypress or jest-axe) Preventing regressions after the audit Your 55 minute budget Block Checks Time Automated scan + triage Sets up 8, 9, 13, 16, 18 5 min Keyboard only pass 1 to 7 15 min Visual and zoom pass 8 to 12 10 min Structure and forms 13 to 20 15 min Screen reader spot check 21, 22 + confirm 16 10 min The 22 checks at a glance # Check WCAG 2.2 criterion Level 1 Everything operable by keyboard 2.1.1 Keyboard A 2 No keyboard traps 2.1.2 No Keyboard Trap A 3 Focus order matches visual order 2.4.3 Focus Order A 4 Visible focus indicator everywhere 2.4.7 Focus Visible AA 5 Focus never hidden by sticky UI 2.4.11 Focus Not Obscured (Minimum) AA (new in 2.2) 6 Working skip link / bypass mechanism 2.4.1 Bypass Blocks A 7 Target size and pointer alternatives 2.5.8, 2.5.7, 2.5.1 AA / A 8 Text contrast 4.5:1 (3:1 large text) 1.4.3 Contrast (Minimum) AA 9 UI component and graphic contrast 3:1 1.4.11 Non-text Contrast AA 10 Colour is never the only cue 1.4.1 Use of Color A 11 Reflow at 400% zoom / 320 CSS px 1.4.10 Reflow AA 12 Text resize and text spacing overrides 1.4.4, 1.4.12 AA 13 Meaningful alt text, empty alt for decoration 1.1.1 Non-text Content A 14 Heading hierarchy and landmarks 1.3.1 Info and Relationships, 2.4.6 A / AA 15 Unique page title and correct lang 2.4.2, 3.1.1 A 16 Accessible name, role and state on every control 4.1.2 Name, Role, Value, 2.5.3 A 17 Link purpose clear from the link text 2.4.4 Link Purpose (In Context) A 18 Every field labelled and autocompleted 3.3.2 Labels or Instructions, 1.3.5 A / AA 19 Errors described in text with a fix suggestion 3.3.1, 3.3.3 A / AA 20 No redundant entry, no cognitive test to log in 3.3.7, 3.3.8 A / AA (new in 2.2) 21 Status messages announced without focus change 4.1.3 Status Messages AA 22 Motion, autoplay and time limits controllable 2.2.1, 2.2.2, 2.3.1, 1.4.2 A Part 1: Keyboard navigation and focus management (checks 1 to 7) Unplug the mouse. Seriously. Put it out of reach for fifteen minutes and drive the page with Tab, Shift+Tab, Enter, Space, arrow keys and Esc. There is a practical rundown of it online. 1. Every interactive element is reachable and operable by keyboard Maps to: 2.1.1 Keyboard (Level A) Test: Tab through the entire page. Activate every control you reach. Then look for anything you never reached: card overlays, custom dropdowns, carousel arrows, star ratings, drag handles, map controls, “x” close buttons. Tool: Accessibility Insights for Web, “Tab stops” visualisation, which draws numbered dots on the page as you tab. Fail if: a <div> or <span> with a click handler has no tabindex=”0″, no role and no key handler, or a control opens only on hover. Fix: use native <button>, <a href>, <input> first. Custom widgets need role, tabindex and Enter/Space/arrow handling as described in the ARIA Authoring Practices Guide. 2. No keyboard traps Maps to: 2.1.2 No Keyboard Trap (Level A) Test: open every modal, date picker, cookie banner, video player and embedded iframe. Tab forward and backward out of each one. Press Esc. Fail if: focus loops forever inside a widget with no keyboard way out, or Esc closes the dialog but focus lands on <body> instead of returning to the trigger element. Note: a modal should trap focus while open, but it must be dismissible with Esc and must restore focus to the element that opened it. 3. Focus order matches the visual order Maps to: 2.4.3 Focus Order (Level A) Test: watch the tab stop numbers. They should read left to right, top to bottom (or the reverse in RTL locales). Fail if: CSS order, flex-direction: row-reverse, grid-area or absolute positioning has reordered the visual layout without changing the DOM, or if a positive tabindex value (2, 3, 10…) exists anywhere in the page. Fix: reorder the DOM. Only tabindex=”0″ and tabindex=”-1″ belong

WCAG Accessibility Checklist for Developers: 22 Checks to Run Before You Ship Read More »

How to Migrate a Database from MySQL to PostgreSQL: A Step-by-Step Guide with pgloader

Deciding to migrate MySQL to PostgreSQL is the easy part. The hard part is doing it without a five hour outage, without breaking half your application queries, and without discovering three weeks later that 40,000 rows quietly turned into NULL because of a zero date. This guide is the checklist we actually use on client migrations at Coding4. It covers schema conversion, the data type mismatches that bite, running pgloader, verifying the data properly (row counts alone are not enough), the application code changes you must make after the switch, and a rollback plan for the moment the cutover goes sideways. Before you touch anything: the pre-migration audit Most failed migrations fail during planning, not during the data copy. Spend a day on this list before you install a single tool. Inventory the schema. Count tables, views, stored procedures, triggers, events and foreign keys. pgloader moves tables, data and indexes. It does not convert stored procedures, triggers or views. Those are manual rewrites. Measure the data. Total size, largest tables, and the number of rows in each. This drives your maintenance window estimate. List every application that touches the database. Cron jobs, BI tools, reporting scripts and that one legacy PHP page nobody owns all count. Find the raw SQL. ORMs abstract a lot, but raw queries with INSERT IGNORE, ON DUPLICATE KEY UPDATE or backticks will break instantly. Check your MySQL version and charset. Tables still on utf8 (3 byte) instead of utf8mb4 may hold mangled emoji. Fix that before migrating, not after. Decide the target version. PostgreSQL 17 and 18 are both solid production choices in 2026. Pick one and standardise your dev, staging and production on it. Quick schema inventory queries — Table sizes and approximate row counts SELECT table_name, table_rows, ROUND((data_length + index_length) / 1024 / 1024) AS size_mb, engine FROM information_schema.tables WHERE table_schema = ‘appdb’ ORDER BY (data_length + index_length) DESC; — Objects pgloader will NOT migrate for you SELECT routine_name, routine_type FROM information_schema.routines WHERE routine_schema = ‘appdb’; SELECT trigger_name, event_object_table FROM information_schema.triggers WHERE trigger_schema = ‘appdb’; SELECT table_name FROM information_schema.views WHERE table_schema = ‘appdb’; — Columns that are likely to cause type trouble SELECT table_name, column_name, column_type FROM information_schema.columns WHERE table_schema = ‘appdb’ AND (column_type LIKE ‘%unsigned%’ OR column_type LIKE ‘enum%’ OR column_type LIKE ‘set%’ OR column_type LIKE ‘tinyint(1)%’ OR data_type IN (‘year’,’bit’,’datetime’,’timestamp’)); Step 1: Understand the data type mismatches This is where silent corruption happens. pgloader has sensible defaults, but defaults are not always what your application expects. Review this mapping table and decide explicitly for each questionable column. MySQL type PostgreSQL target What to watch out for TINYINT(1) boolean pgloader converts it by default. If you store values other than 0 and 1 in it, you will lose data. Check first. INT UNSIGNED bigint PostgreSQL has no unsigned types. Values above 2,147,483,647 overflow an integer. Promote to bigint. DATETIME timestamptz or timestamp Decide on timezone semantics before loading. Mixing them later is painful. 0000-00-00 dates NULL Invalid in PostgreSQL. Use the zero-dates-to-null transform and make sure app code handles NULL. ENUM native enum type or text + CHECK pgloader creates a native enum. Adding values later is a DDL change, so text plus a check constraint is often more flexible. SET text[] or junction table No direct equivalent. Plan a manual transformation. JSON jsonb Key order is not preserved in jsonb. If anything depends on key order, use json. DOUBLE for money numeric(18,2) Good moment to fix a legacy mistake. Rounding will change, so validate financial totals. YEAR smallint No YEAR type in PostgreSQL. BIT(1) boolean Check driver behaviour, some ORMs return bytes. TEXT, VARCHAR(n) text In PostgreSQL text has no performance penalty. Keep length limits only where they are a real business rule. AUTO_INCREMENT identity column + sequence Sequences must be reset after load or your first insert throws a duplicate key error. The case sensitivity trap MySQL string comparison is usually case insensitive (utf8mb4_general_ci). PostgreSQL is case sensitive. A login lookup like WHERE email = ‘[email protected]’ that worked in MySQL will return zero rows in PostgreSQL. Options: Normalise values at write time (lowercase emails and usernames). Use citext for the affected columns: CREATE EXTENSION citext; Use functional indexes: CREATE INDEX ON users (lower(email)); and query with lower(email) = lower($1). Also note that unquoted identifiers are folded to lowercase in PostgreSQL. A MySQL table named UserProfiles becomes userprofiles after pgloader unless you force quoting. Consistent snake_case naming is the safest outcome. Step 2: Install pgloader pgloader is a single command line tool that reads directly from MySQL over the wire and writes into PostgreSQL using COPY. No intermediate dump file needed. # Ubuntu / Debian sudo apt update && sudo apt install -y pgloader # macOS brew install pgloader # Docker (recommended for reproducibility and recent versions) docker run –rm -it \ -v “$PWD”:/work -w /work \ ghcr.io/dimitri/pgloader:latest pgloader –version Tip for Windows users: run pgloader inside WSL2 or Docker. Native Windows builds are not maintained and you will waste hours. Step 3: Prepare the PostgreSQL target CREATE ROLE app_owner LOGIN PASSWORD ‘strong-password’; CREATE DATABASE appdb OWNER app_owner ENCODING ‘UTF8’ LC_COLLATE ‘C’ LC_CTYPE ‘C’ TEMPLATE template0; \c appdb CREATE EXTENSION IF NOT EXISTS citext; CREATE EXTENSION IF NOT EXISTS pg_stat_statements; CREATE EXTENSION IF NOT EXISTS pg_trgm; For the load itself, temporarily give the instance more room. These are session or restart level settings you revert afterwards: — postgresql.conf tweaks for the migration window only maintenance_work_mem = ‘2GB’ max_wal_size = ’16GB’ checkpoint_timeout = ’30min’ autovacuum = off — turn it back ON straight after the load synchronous_commit = off On the MySQL side, create a read-only user for pgloader and make sure it can read from a replica rather than the primary if your dataset is large. Step 4: Write the pgloader load file Do not use the one line pgloader mysql://… postgresql://… form for a production migration. A load file is repeatable, reviewable and versionable in Git. LOAD DATABASE FROM mysql://migrator:[email protected]/appdb INTO postgresql://app_owner:[email protected]/appdb WITH include drop, create tables, create indexes,

How to Migrate a Database from MySQL to PostgreSQL: A Step-by-Step Guide with pgloader Read More »

How to Build a REST API with Node.js and Express: A Complete Beginner’s Tutorial

How to Build a REST API with Node.js and Express: A Hands-On Task Manager Tutorial If you are wondering how to build a REST API with Node.js, this guide will walk you through the entire process by building something real: a simple task manager API. No abstract theory, no filler. By the end, you will have a working API with routes, a database, and tested endpoints in Postman. This tutorial is written for beginners with basic JavaScript knowledge. We will use the latest stable versions of Node.js and Express available in 2026. (via https://blog.postman.com) What You Will Build A REST API for a task manager that supports the following operations: Create a new task Retrieve all tasks Retrieve a single task by ID Update an existing task Delete a task We will map these actions to standard HTTP methods: Action HTTP Method Endpoint Create task POST /api/tasks Get all tasks GET /api/tasks Get one task GET /api/tasks/:id Update task PUT /api/tasks/:id Delete task DELETE /api/tasks/:id Prerequisites Node.js 22 LTS or later installed (check with node -v) A code editor such as VS Code Postman installed for testing endpoints A free MongoDB Atlas account (or a local MongoDB instance) Step 1: Scaffold the Node.js Project Create a new folder and initialize a Node.js project: mkdir task-manager-api cd task-manager-api npm init -y Open the generated package.json and add “type”: “module” so we can use modern ES module syntax. Step 2: Install Dependencies We only need a few packages to get started: npm install express mongoose dotenv npm install –save-dev nodemon Here is what each package does: express: the web framework for routing and middleware mongoose: an ODM to interact with MongoDB dotenv: loads environment variables from a .env file nodemon: restarts the server automatically during development Add these scripts to your package.json: “scripts”: { “start”: “node server.js”, “dev”: “nodemon server.js” } Step 3: Create the Server Entry Point Create a file called server.js in the root of your project: import express from ‘express’; import mongoose from ‘mongoose’; import dotenv from ‘dotenv’; import taskRoutes from ‘./routes/tasks.js’; dotenv.config(); const app = express(); app.use(express.json()); app.use(‘/api/tasks’, taskRoutes); app.get(‘/’, (req, res) => { res.json({ message: ‘Task Manager API is running’ }); }); const PORT = process.env.PORT || 3000; mongoose.connect(process.env.MONGO_URI) .then(() => { app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); }) .catch(err => console.error(‘Database connection failed:’, err)); Step 4: Configure Environment Variables Create a .env file at the project root: PORT=3000 MONGO_URI=mongodb+srv://YOUR_USER:[email protected]/taskmanager Never commit this file. Add .env to your .gitignore. Step 5: Define the Task Model Create a folder called models and inside it a file called Task.js: import mongoose from ‘mongoose’; const taskSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, description: { type: String, default: ” }, completed: { type: Boolean, default: false } }, { timestamps: true }); export default mongoose.model(‘Task’, taskSchema); Step 6: Build the Routes Create a folder called routes with a file called tasks.js: import express from ‘express’; import Task from ‘../models/Task.js’; const router = express.Router(); // Create a task router.post(‘/’, async (req, res) => { try { const task = await Task.create(req.body); res.status(201).json(task); } catch (err) { res.status(400).json({ error: err.message }); } }); // Get all tasks router.get(‘/’, async (req, res) => { const tasks = await Task.find().sort({ createdAt: -1 }); res.json(tasks); }); // Get one task router.get(‘/:id’, async (req, res) => { try { const task = await Task.findById(req.params.id); if (!task) return res.status(404).json({ error: ‘Task not found’ }); res.json(task); } catch (err) { res.status(400).json({ error: ‘Invalid ID’ }); } }); // Update a task router.put(‘/:id’, async (req, res) => { try { const task = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); if (!task) return res.status(404).json({ error: ‘Task not found’ }); res.json(task); } catch (err) { res.status(400).json({ error: err.message }); } }); // Delete a task router.delete(‘/:id’, async (req, res) => { try { const task = await Task.findByIdAndDelete(req.params.id); if (!task) return res.status(404).json({ error: ‘Task not found’ }); res.json({ message: ‘Task deleted’ }); } catch (err) { res.status(400).json({ error: ‘Invalid ID’ }); } }); export default router; Step 7: Start the Server Run the development server: npm run dev If everything is configured correctly, you should see Server running on port 3000 in your terminal. There’s a good explainer over at dev.to. Step 8: Test Your Endpoints with Postman Open Postman and try the following requests one by one. Create a task (POST) URL: http://localhost:3000/api/tasks Body (raw JSON): { “title”: “Write blog post”, “description”: “Publish the Node.js tutorial on coding4.net” } Get all tasks (GET) Send a GET request to http://localhost:3000/api/tasks and you should see an array containing the task you just created. Update a task (PUT) Copy the _id from the response and send: PUT http://localhost:3000/api/tasks/THE_ID_HERE { “completed”: true } Delete a task (DELETE) Send a DELETE request to http://localhost:3000/api/tasks/THE_ID_HERE. You should receive a confirmation message. Step 9: Add Basic Error Handling Middleware Robust APIs handle unexpected errors gracefully. Add this at the bottom of server.js, just before mongoose.connect: app.use((req, res) => { res.status(404).json({ error: ‘Route not found’ }); }); app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: ‘Internal server error’ }); }); Best Practices to Keep in Mind Use versioning in URLs, for example /api/v1/tasks, so future changes do not break clients Validate input with a library like Zod or Joi before hitting the database Return proper HTTP status codes (201 for created, 404 for not found, 400 for bad request) Never store secrets in the codebase, always use environment variables Add authentication with JWT once your API grows past a basic prototype Enable CORS if a frontend from another domain will consume the API Project Structure Recap task-manager-api/ ├── models/ │ └── Task.js ├── routes/ │ └── tasks.js ├── .env ├── .gitignore ├── package.json └── server.js Next Steps You now have a working REST API. Here are ideas to push it further: Add user authentication with JWT tokens Deploy it to Render, Railway, or Fly.io Write automated tests with Vitest or Jest Document the endpoints with Swagger

How to Build a REST API with Node.js and Express: A Complete Beginner’s Tutorial Read More »

How to Prevent XSS Attacks in JavaScript: 6 Techniques With Code Examples

Cross-site scripting (XSS) remains one of the most exploited vulnerabilities on the web, and JavaScript apps are still the primary attack surface in 2026. If you’re building modern SPAs, Node.js APIs, or hybrid apps, learning how to prevent XSS attacks in JavaScript is not optional, it’s a core engineering skill. This guide skips the theory and gives you 6 actionable defense techniques you can ship today, each with working code examples. It is argued more carefully on mozilla.org. What Is an XSS Attack in JavaScript? An XSS attack happens when an attacker injects malicious JavaScript into your web application, which is then executed in the browser of another user. The result can be session hijacking, credential theft, defacement, or full account takeover. There are three main types you should recognize: Type How it works Typical vector Stored XSS Malicious script saved on the server and served to users Comments, profiles, forum posts Reflected XSS Script reflected from URL or form back into the page Search fields, error pages DOM-based XSS Vulnerability lives entirely in client-side JS innerHTML, location.hash, document.write Now let’s fix them. 1. Never Use innerHTML with Untrusted Data The single most common source of DOM-based XSS is innerHTML. If you insert user input directly, you hand attackers the keys. Vulnerable code: const username = new URLSearchParams(location.search).get(‘name’); document.getElementById(‘greeting’).innerHTML = ‘Hello ‘ + username; // URL: ?name=<img src=x onerror=alert(1)> → executes Safe alternative using textContent: const username = new URLSearchParams(location.search).get(‘name’); document.getElementById(‘greeting’).textContent = ‘Hello ‘ + username; // The <img> tag is rendered as literal text, not HTML Rule of thumb: Use textContent when you want text Use setAttribute when you want attributes Use createElement + appendChild for structured DOM Reserve innerHTML for static, developer-controlled strings only 2. Sanitize HTML with DOMPurify When You Really Need Rich Content Sometimes you actually need to render user-submitted HTML (rich text editors, markdown, emails). Don’t roll your own sanitizer. Use DOMPurify, the de-facto standard. import DOMPurify from ‘dompurify’; const dirty = userSubmittedHtml; // e.g. from a WYSIWYG editor const clean = DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [‘b’, ‘i’, ’em’, ‘strong’, ‘a’, ‘p’, ‘ul’, ‘li’, ‘br’], ALLOWED_ATTR: [‘href’, ‘title’] }); document.getElementById(‘post’).innerHTML = clean; DOMPurify strips <script> tags, on* event handlers, javascript: URLs, and dozens of other bypass tricks that a homemade regex will miss. Don’t try to sanitize with regex Every year a new developer writes something like input.replace(/<script>/gi, ”) and every year attackers bypass it with <ScRipT>, <svg onload>, or encoded payloads. Just use a maintained library. 3. Encode Output Based on Context The same string is dangerous in different ways depending on where you inject it. Encode it for its context: Context Encoding required HTML body & < > ” ‘ → HTML entities HTML attribute HTML entity + always quote the attribute URL parameter encodeURIComponent() Inline JavaScript JSON.stringify() then HTML-encode CSS value Whitelist-based, avoid entirely if possible Example of a simple HTML encoder: function escapeHtml(str) { return String(str) .replace(/&/g, ‘&amp;’) .replace(/</g, ‘&lt;’) .replace(/>/g, ‘&gt;’) .replace(/”/g, ‘&quot;’) .replace(/’/g, ‘&#39;’); } 4. Deploy a Strict Content Security Policy (CSP) A well-configured CSP is your last line of defense. Even if an attacker sneaks a payload through, CSP can block execution. Send this header from your server: Content-Security-Policy: default-src ‘self’; script-src ‘self’ ‘nonce-r4nd0m123’; object-src ‘none’; base-uri ‘self’; frame-ancestors ‘none’; Then reference scripts with the matching nonce: <script nonce=”r4nd0m123″> // your legitimate inline script </script> Key rules for a strong CSP in 2026: Avoid ‘unsafe-inline’ and ‘unsafe-eval’ Use nonces or hashes instead of allowlisting domains where possible Always set object-src ‘none’ Add base-uri ‘self’ to prevent base tag injection Report violations with report-to before enforcing 5. Enable Trusted Types for DOM XSS Elimination Trusted Types is now widely supported in Chromium and Firefox browsers, and it’s the most powerful browser-level defense against DOM XSS. It forces every dangerous sink (innerHTML, script.src, etc.) to accept only vetted, policy-approved values. Enable it via CSP: Content-Security-Policy: require-trusted-types-for ‘script’; trusted-types default dompurify; Then define a policy in your app: import DOMPurify from ‘dompurify’; if (window.trustedTypes && trustedTypes.createPolicy) { trustedTypes.createPolicy(‘default’, { createHTML: (input) => DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true }) }); } // Now this is automatically sanitized: element.innerHTML = untrustedInput; Any code that tries to assign a raw string to innerHTML without going through the policy will throw a TypeError. This is game-changing for large codebases. 6. Use Framework-Native Escaping Correctly Modern frameworks escape by default, but they all have escape hatches that are XSS landmines. React // Safe: automatically escaped <div>{userInput}</div> // DANGEROUS: bypasses escaping <div dangerouslySetInnerHTML={{ __html: userInput }} /> // Safer version if you must render HTML: <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} /> Vue <!– Safe –> <div>{{ userInput }}</div> <!– DANGEROUS –> <div v-html=”userInput”></div> Angular Angular auto-sanitizes bound HTML. Never call bypassSecurityTrustHtml() on untrusted input. Bonus: Extra Defensive Measures HttpOnly cookies: prevent JavaScript from reading session tokens SameSite=Strict or Lax: reduces impact of stolen sessions Secure flag: cookies transmitted only over HTTPS Subresource Integrity (SRI): verify third-party scripts haven’t been tampered with X-Content-Type-Options: nosniff: prevents MIME confusion attacks Regular dependency audits: run npm audit or Snyk in CI Quick XSS Prevention Checklist Replace every innerHTML with textContent where possible Sanitize all rich HTML input with DOMPurify Context-aware output encoding on both client and server Deploy a strict, nonce-based CSP Enable Trusted Types in supported browsers Never trust framework escape hatches with user data Set HttpOnly, Secure, SameSite on all cookies Audit dependencies regularly FAQ What is the best protection against XSS in JavaScript? There is no single silver bullet. The best protection combines context-aware output encoding, HTML sanitization with a library like DOMPurify, a strict Content Security Policy with nonces, and Trusted Types to eliminate DOM XSS sinks entirely. Is XSS still relevant in 2026? Yes. XSS remains in the OWASP Top 10 and continues to be one of the most reported web vulnerabilities. Modern SPAs, third-party widgets, and browser extensions all introduce new attack surfaces every year. Does React fully protect me from XSS? No. React escapes text nodes and attributes by default, but dangerouslySetInnerHTML, href with

How to Prevent XSS Attacks in JavaScript: 6 Techniques With Code Examples Read More »

How to Use CSS Flexbox: A Beginner’s Guide With 12 Practical Examples

If you have ever spent hours trying to center a div or align items in a row, this CSS Flexbox tutorial is for you. Instead of drowning you in theory, we are going to build 12 real layouts you can copy, paste and tweak. By the end of this guide, you will be comfortable creating navigation bars, card grids and responsive designs using nothing but Flexbox. What is CSS Flexbox in Simple Terms? Flexbox (Flexible Box Layout) is a one-dimensional layout system for arranging items in a row or a column. It lets a container distribute space between its children automatically, even when their size is unknown or dynamic. You only need to know two concepts to get started: Flex container: the parent element with display: flex. Flex items: the direct children of that container. Setting Up Your First Flex Container Before jumping into examples, here is the base HTML we will reuse throughout this tutorial: <div class=”container”> <div class=”item”>1</div> <div class=”item”>2</div> <div class=”item”>3</div> </div> And the minimal CSS to activate Flexbox: .container { display: flex; } That single line already places your items in a row. Now let’s build real things. Example 1: Horizontal Navigation Bar The classic use case. Items sit in a row with spacing between them. .navbar { display: flex; gap: 20px; padding: 15px; background: #222; } .navbar a { color: white; text-decoration: none; } Example 2: Centering Anything (Vertically and Horizontally) The famous centering problem, solved in 3 lines: .center { display: flex; justify-content: center; align-items: center; height: 100vh; } justify-content handles the main axis (horizontal by default). align-items handles the cross axis (vertical by default). Example 3: Push One Item to the Right Perfect for a logo on the left and a login button on the right. .header { display: flex; } .login-btn { margin-left: auto; } Example 4: Equal Width Columns with flex-grow Want three columns that share the space equally regardless of content? .item { flex: 1; } The flex: 1 shorthand means each item grows to fill available space equally. This is one of the most useful lines in modern CSS. Example 5: Sidebar + Main Content Layout .layout { display: flex; } .sidebar { flex: 0 0 250px; } .main { flex: 1; } The sidebar keeps a fixed 250px width, the main content takes the rest. Example 6: Card Grid with flex-wrap When you want items to wrap to the next line if they do not fit: .cards { display: flex; flex-wrap: wrap; gap: 20px; } .card { flex: 1 1 300px; } Each card will try to be 300px wide, grow if there is space, and wrap to a new row when needed. Example 7: Vertical Stack (Column Direction) .stack { display: flex; flex-direction: column; gap: 10px; } When flex-direction is set to column, the main axis becomes vertical. That means justify-content now controls vertical alignment. Implementing this cleanly is bread-and-butter for a capable development team. Example 8: Sticky Footer Keep the footer at the bottom even when content is short: body { display: flex; flex-direction: column; min-height: 100vh; } main { flex: 1; } Example 9: Space Between, Around and Evenly Here is a quick reference for the most useful justify-content values: Value Behavior flex-start Items packed at the start flex-end Items packed at the end center Items centered space-between First and last item at edges, equal gaps between space-around Equal space around each item space-evenly Equal space between and at edges Example 10: Reordering Items Without Touching HTML The order property lets you rearrange items visually: .item:nth-child(1) { order: 3; } .item:nth-child(2) { order: 1; } .item:nth-child(3) { order: 2; } Very handy for responsive designs where the mobile order differs from the desktop order. Example 11: Different Alignment for a Single Item Use align-self to override align-items for one specific item: .container { display: flex; align-items: flex-start; height: 200px; } .special { align-self: center; } Example 12: Fully Responsive Layout Without Media Queries Combining everything we learned: .responsive { display: flex; flex-wrap: wrap; gap: 16px; } .responsive > * { flex: 1 1 250px; } On large screens, you get multiple columns. On mobile, items stack automatically. No @media queries required. Understanding the Flex Shorthand The flex property is a shorthand for three values: flex-grow: how much the item grows relative to others flex-shrink: how much it shrinks when space is tight flex-basis: the initial size before growing or shrinking So flex: 1 1 200px means: grow equally, shrink equally, start at 200px. Flexbox vs CSS Grid: When to Use Which? Use Flexbox when… Use Grid when… You have a single row or column You need rows AND columns Content size drives the layout Layout drives content placement Navbars, toolbars, card lists Full page layouts, dashboards They are not competitors. Most modern sites use both together. Common Mistakes to Avoid Forgetting that flex properties apply to direct children only. Using height: 100% instead of letting Flexbox handle sizing. Mixing up main axis and cross axis after changing flex-direction. Using margins for spacing instead of the modern gap property. FAQ Is Flexbox hard to learn? Not at all. Once you understand the container/item relationship and the main/cross axis concept, you can build 90% of common layouts within a few hours of practice. Is Flexbox still relevant in 2026? Absolutely. Flexbox is supported in every modern browser and remains the standard for one-dimensional layouts. It works perfectly alongside CSS Grid and container queries. It is argued more carefully on mozilla.org. What is the difference between justify-content and align-items? justify-content aligns items along the main axis (horizontal by default). align-items aligns them along the cross axis (vertical by default). If you switch to flex-direction: column, their roles swap. Can I use Flexbox for a full page layout? Yes, but for two-dimensional layouts (rows and columns together), CSS Grid is usually cleaner. Many developers use Grid for the overall page structure and Flexbox inside components. What does flex: 1 actually do? It is a shorthand for flex: 1

How to Use CSS Flexbox: A Beginner’s Guide With 12 Practical Examples Read More »

How to Set Up a Git Branching Strategy for Small Teams: GitFlow vs Trunk-Based Development

If you work in a small dev team (2 to 10 people), your git branching strategy can either accelerate delivery or become a daily bottleneck. Too much process kills momentum. Too little creates chaos on main. This guide compares GitFlow and trunk-based development, gives you concrete naming conventions, merge workflows, and real command examples so you can pick what fits your release cadence without overengineering. Why Your Branching Strategy Matters (Even in a 3-Person Team) A branching strategy is a shared contract. It defines where code lives, how it gets reviewed, and when it ships. Small teams often skip this conversation and end up with: Long-lived feature branches that drift from main Painful merge conflicts on Friday afternoons Unclear release process (“what’s actually in production?”) Hotfixes applied directly to production with no traceability Pick a strategy early, document it in your CONTRIBUTING.md, and revisit it every 6 months. The Two Realistic Options for Small Teams Forget the 6-branch diagrams you saw on LinkedIn. For a small team, it comes down to two workable models: 1. GitFlow (structured, release-oriented) Introduced by Vincent Driessen, GitFlow uses multiple long-lived branches: main reflects production develop is the integration branch feature/* branches off develop release/* stabilizes a version before shipping hotfix/* patches production directly Best when: you ship on a fixed schedule (weekly, monthly), you support multiple versions in production, or you have QA gates. 2. Trunk-Based Development (fast, continuous) Everyone commits to main (the trunk) via short-lived branches that live less than 2 days. Feature flags hide unfinished work. Releases are cut from main as tags. There’s a good explainer over at dev.to. Best when: you deploy multiple times per week or per day, you have solid CI, and you trust your test suite. Side-by-Side Comparison Criteria GitFlow Trunk-Based Release cadence Scheduled (weekly/monthly) Continuous (daily+) Branch lifespan Days to weeks Hours to 2 days max Merge conflicts Frequent, larger Rare, small CI/CD required Nice to have Mandatory Feature flags Optional Highly recommended Learning curve Higher Lower Fits SaaS web apps Overkill Ideal Fits mobile / desktop / embedded Ideal Possible with tags Branch Naming Conventions That Actually Scale Whichever strategy you pick, enforce a naming convention. It makes filtering, automation, and code review much easier. Recommended pattern <type>/<ticket-id>-<short-description> feature/PROJ-142-add-oauth-login bugfix/PROJ-198-fix-null-user-avatar hotfix/PROJ-210-stripe-webhook-500 chore/upgrade-node-22 refactor/extract-payment-service Rules Lowercase only, dashes as separators Always prefix with the branch type Include the ticket ID (Jira, Linear, GitHub Issues) for traceability Keep it under 50 characters Delete the branch after merge (enable auto-delete in GitHub/GitLab) GitFlow in Practice: Real Commands Starting a feature git checkout develop git pull origin develop git checkout -b feature/PROJ-142-add-oauth-login # work, commit, push git push -u origin feature/PROJ-142-add-oauth-login Opening the PR and merging Open a Pull Request from feature/PROJ-142-add-oauth-login into develop. After review and green CI: git checkout develop git pull origin develop git merge –no-ff feature/PROJ-142-add-oauth-login git push origin develop git branch -d feature/PROJ-142-add-oauth-login Cutting a release git checkout develop git checkout -b release/1.4.0 # bump version, update changelog, fix last minute bugs git checkout main git merge –no-ff release/1.4.0 git tag -a v1.4.0 -m “Release 1.4.0” git push origin main –tags git checkout develop git merge –no-ff release/1.4.0 git push origin develop Emergency hotfix git checkout main git checkout -b hotfix/PROJ-210-stripe-webhook-500 # fix, commit git checkout main git merge –no-ff hotfix/PROJ-210-stripe-webhook-500 git tag -a v1.4.1 -m “Hotfix 1.4.1” git push origin main –tags git checkout develop git merge –no-ff hotfix/PROJ-210-stripe-webhook-500 git push origin develop Trunk-Based Development in Practice The core loop Pull latest main Create a short-lived branch Commit small and often Open a PR the same day Merge (squash) into main after review and CI Delete the branch Commands git checkout main git pull –rebase origin main git checkout -b feature/PROJ-142-oauth # small commits git push -u origin feature/PROJ-142-oauth # open PR, get review, CI green # merge via GitHub “Squash and merge” git checkout main git pull –rebase origin main Releasing Tag any commit on main that passes production checks: git checkout main git pull git tag -a v2026.07.28 -m “Production release” git push origin –tags Feature flags: your safety net Merge unfinished code behind a flag so main stays deployable: There’s a fuller breakdown if you want the detail. if (featureFlags.isEnabled(“new-checkout”)) { renderNewCheckout(); } else { renderLegacyCheckout(); } Tools like Unleash, LaunchDarkly, or a simple env-based flag work perfectly. How to Choose: A Decision Framework Ask yourself these five questions: How often do we deploy? More than 2x/week means trunk-based. Do we maintain multiple versions in production? Yes means GitFlow. Is our CI reliable (tests, linting, security scan)? No means GitFlow until you fix CI. Do we have manual QA before release? Yes suggests GitFlow release branches. Are we shipping a SaaS web app? Trunk-based is almost always the answer. Our Recommendation for Most Small Teams in 2026 For the majority of small teams building web applications or internal tools, we recommend a simplified trunk-based approach, often called GitHub Flow: You can read more here. One long-lived branch: main Short-lived feature/* and bugfix/* branches (max 2 days) Mandatory PR with at least 1 reviewer Squash-and-merge to keep history clean Protected main: no direct push, CI must pass Tag releases with semantic versioning If you build embedded software, mobile apps sold through app stores, or products with long-term support versions, stick with GitFlow. Common Mistakes to Avoid Long-lived feature branches. If a branch lives more than 3 days, split the work. No branch protection. Always require PR reviews and passing CI on main. Merging without rebasing. Keep history linear when possible (squash or rebase). Skipping tags. Tags are how you answer “what’s in production right now?” Copying enterprise workflows. Your team is not Google. Simpler is better. FAQ Is GitFlow dead in 2026? No. Vincent Driessen himself noted GitFlow is not ideal for web SaaS but remains valuable for versioned software (mobile apps, libraries, embedded systems). It’s not dead, just misapplied. Can a 2-person team use trunk-based development? Absolutely. In fact, trunk-based works even better with fewer people because coordination

How to Set Up a Git Branching Strategy for Small Teams: GitFlow vs Trunk-Based Development Read More »

How to Set Up Sentry for Error Tracking in a Node.js Production App

Why Sentry Node.js Error Tracking Matters in Production When a Node.js app crashes at 3 AM, your logs alone rarely tell the full story. Sentry Node.js error tracking gives you real-time stack traces, user context, breadcrumbs, and performance insights so you can fix bugs before your users complain on Twitter. In this hands-on tutorial, we at Coding4 will walk you through installing, configuring, and using Sentry in a real Node.js production application. Unlike other guides, we will also cover the things that actually bite you in production: source map uploads, smart alerting, and filtering noisy errors so your inbox stays sane. namastedev.com has a solid rundown on this. What You Will Build A Node.js (Express) app instrumented with the latest Sentry SDK Automatic error and unhandled rejection capture Performance monitoring with distributed tracing Source maps uploaded on every deploy Alert rules that only ping you when it truly matters Filters to ignore known noise (bots, health checks, ECONNRESET, etc.) Step 1: Create a Sentry Project Sign in at sentry.io and create a new project. Select Node.js as the platform (or Express if that fits better). Copy the generated DSN. You will need it in a moment. Step 2: Install the Sentry SDK For any modern Node.js project (Node 18+ recommended in 2026), install the official SDK: npm install @sentry/node @sentry/profiling-node If you use TypeScript, no extra types package is needed. Sentry ships them out of the box. Step 3: Initialize Sentry as Early as Possible Create a dedicated instrument.js (or instrument.ts) file. It must be imported before any other module so Sentry can auto-instrument your dependencies. // instrument.js const Sentry = require(‘@sentry/node’); const { nodeProfilingIntegration } = require(‘@sentry/profiling-node’); Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV || ‘development’, release: process.env.APP_RELEASE, // e.g. ‘[email protected]’ integrations: [nodeProfilingIntegration()], tracesSampleRate: 0.2, // 20% of transactions profilesSampleRate: 0.2, // 20% of sampled transactions sendDefaultPii: false }); Then in your entry file: // server.js require(‘./instrument’); const express = require(‘express’); const Sentry = require(‘@sentry/node’); const app = express(); app.get(‘/’, (req, res) => res.send(‘Hello’)); app.get(‘/boom’, () => { throw new Error(‘Test error from Coding4’); }); // The Sentry error handler must be registered before any other error middleware Sentry.setupExpressErrorHandler(app); app.use((err, req, res, next) => { res.statusCode = 500; res.end(‘Internal Server Error’); }); app.listen(3000); Hit /boom once. You should see the error appear in your Sentry dashboard within seconds. This guide goes deeper on it. Step 4: Capture Unhandled Rejections and Manual Errors The SDK already hooks uncaughtException and unhandledRejection. For business logic errors you want to capture manually, use: try { await chargeCustomer(order); } catch (err) { Sentry.captureException(err, { tags: { module: ‘billing’ }, extra: { orderId: order.id } }); throw err; } Step 5: Upload Source Maps on Every Deploy Without source maps, minified or transpiled stack traces are useless. The recommended way in 2026 is the Sentry Wizard: npx @sentry/wizard@latest -i sourcemaps It will: Create a .sentryclirc or environment variable setup Add build scripts using @sentry/cli Inject Debug IDs so source maps match no matter the release name Add these environment variables to your CI/CD: Variable Purpose SENTRY_AUTH_TOKEN Auth token with project:releases scope SENTRY_ORG Your Sentry org slug SENTRY_PROJECT Project slug APP_RELEASE Unique release identifier, e.g. git SHA In your CI pipeline, after a successful build: npx sentry-cli sourcemaps inject ./dist npx sentry-cli sourcemaps upload –release=$APP_RELEASE ./dist Step 6: Filter the Noise Nothing kills adoption faster than a dashboard full of junk. Here are filters we apply on almost every Coding4 project: Sentry.init({ // … ignoreErrors: [ ‘ECONNRESET’, ‘ETIMEDOUT’, ‘Non-Error promise rejection captured’ ], beforeSend(event, hint) { const err = hint.originalException; // Drop 404s and health check noise if (event.request?.url?.includes(‘/health’)) return null; if (err && err.status === 404) return null; // Drop known bot user agents const ua = event.request?.headers?.[‘user-agent’] || ”; if (/bot|crawler|spider/i.test(ua)) return null; return event; } }); Sampling Strategy For high-traffic services, sample smartly instead of dropping data blindly: tracesSampler: (ctx) => { if (ctx.request?.url?.includes(‘/health’)) return 0; if (ctx.request?.url?.includes(‘/checkout’)) return 1.0; // critical return 0.1; } Step 7: Configure Alerts That Actually Wake You Up In the Sentry UI, go to Alerts > Create Alert. We recommend the following baseline rules: New issue in production – notify Slack channel #alerts Issue affects more than 50 users in 1 hour – page on-call via PagerDuty or Opsgenie Regression detected – notify the author of the last release Performance: p95 latency > 2s for 5 minutes on critical endpoints Tie each alert to an environment tag (production only) to prevent staging noise. Step 8: Add Rich Context Context is what turns a stack trace into a fix. Add user and request context in your middleware: app.use((req, res, next) => { Sentry.setUser({ id: req.user?.id, email: req.user?.email }); Sentry.setTag(‘tenant’, req.tenantId); next(); }); Coding4 vs Basic Setup: What You Gain Feature Default Install Coding4 Production Setup Error capture Yes Yes + rich context Source maps Manual Automated via CI with Debug IDs Noise filtering None ignoreErrors + beforeSend + tracesSampler Alerts Email everything Tiered alerts by severity and environment Performance Off Distributed tracing + profiling Common Pitfalls to Avoid Initializing Sentry too late: it must load before Express, HTTP, and any DB driver. Missing release tag: without it, source maps will not resolve. 100% sampling in production: expensive and often useless. Sample smartly. Sending PII by accident: keep sendDefaultPii: false unless you truly need it and you are compliant. Not scrubbing secrets: use beforeSend to strip tokens from URLs and headers. FAQ Is Sentry free for Node.js? Yes, Sentry offers a free Developer plan with a monthly event quota. It is enough to get started and to instrument small services. Larger production workloads usually move to the Team or Business plan. For a real-world example, look at one agency that does this well. Does Sentry slow down my Node.js app? The overhead is minimal for error capture. Performance tracing and profiling add some cost, which is why you should use tracesSampleRate and profilesSampleRate instead of capturing 100% of traffic. How is Sentry different from a logging tool like Winston or

How to Set Up Sentry for Error Tracking in a Node.js Production App Read More »

How to Estimate Web Development Project Timelines: A Practical Framework for Agencies and Freelancers

Estimating web development projects is one of the hardest skills to master, yet it is what separates profitable agencies and freelancers from those who constantly work overtime for free. At Coding4.net, we have delivered hundreds of client projects, and we have refined a practical estimation framework that consistently keeps us within budget and on schedule. This guide walks you through exactly how we do it. Instead of giving you a generic online calculator, we are going to show you the real thinking process behind a solid estimate, including how to handle the two things most estimators forget: buffer multipliers and client feedback cycles. Why Most Web Development Estimates Fail Before diving into the framework, let us look at why estimates go wrong so often: Optimism bias: Developers estimate the happy path, not reality. Missing tasks: Setup, deployment, testing, and revisions are often forgotten. Ignoring the client factor: Feedback loops, late assets, and scope changes are guaranteed. No buffer: Estimates are given as fixed numbers instead of ranges with contingency. The framework below fixes all four issues. The 5-Step Framework to Estimate a Web Development Project Step 1: Gather the Right Information Before Estimating You cannot estimate what you do not understand. Before touching a spreadsheet, get clear answers to these questions: There’s a good explainer over at astuteo.com. What is the business goal of the site or application? How many pages or screens are required? Is there an existing design, or does it need to be created? What CMS or framework will be used? What integrations are needed (payment, CRM, analytics, ERP)? Who is providing the content, and when? What is the approval process on the client side? What are the hosting and deployment requirements? If any answer is vague, flag it. Vague answers become scope creep later. Step 2: Break the Project Into Small Tasks (WBS) Use a Work Breakdown Structure. The rule of thumb: no single task should exceed 8 hours. If it does, break it down further. Small tasks are easier to estimate accurately and easier to track. Typical categories for a business website include: Discovery and planning UX and UI design Frontend development Backend and CMS setup Integrations Content integration Testing and QA Deployment and launch Post-launch support Step 3: Apply Buffer Multipliers This is the step most freelancers skip, and it is why they lose money. After estimating each task with your best-case number, multiply it by a buffer factor based on uncertainty. Task Type Uncertainty Level Buffer Multiplier Routine work you have done many times Low x 1.2 Familiar work with some new elements Medium x 1.5 New tech, new integration, unclear specs High x 2.0 R&D, experimental features Very High x 3.0 Step 4: Account for Client Feedback Cycles Client feedback is not a delay, it is part of the project. Yet most estimates ignore it. Here is how we handle it: This write-up is worth a look. Every deliverable gets 2 rounds of revisions built into the estimate. Add calendar days for client review time (usually 2 to 5 business days per round). Distinguish clearly between working hours (what you bill) and calendar time (what the client sees on the timeline). Add a clause in your contract: extra revision rounds are billed separately. A useful rule: for every 100 hours of development work, expect 20 to 30 hours tied up in feedback, meetings, and revisions. Step 5: Convert Hours Into a Realistic Timeline Even if a project totals 200 hours, that does not mean 5 weeks of calendar time. Nobody codes 40 focused hours per week. Use these conversion rules: Productive hours per day: 5 to 6 for a solo developer Add: client review windows, holidays, parallel projects Add: 10 to 15 percent contingency at the project level for unknowns Sample Estimation Breakdown: A Typical Business Website Let us apply the framework to a realistic scenario: a 10-page business website for a mid-sized services company, built on WordPress, with a contact form, blog, newsletter integration, and multilingual support (EN/FR). Phase / Task Base Hours Buffer Final Hours Discovery workshop and requirements doc 6 x 1.2 7 Wireframes (10 pages) 10 x 1.2 12 UI design (desktop + mobile) 24 x 1.5 36 WordPress setup and theme foundation 8 x 1.2 10 Frontend development (10 templates) 40 x 1.5 60 Custom fields, blog, contact form 12 x 1.5 18 Multilingual setup (EN/FR) 8 x 2.0 16 Newsletter integration (Mailchimp/Brevo) 4 x 1.5 6 Content integration 10 x 1.2 12 SEO basics, performance, accessibility 8 x 1.5 12 QA and cross-browser testing 10 x 1.5 15 Deployment and launch 6 x 1.5 9 Project management and client meetings 20 x 1.2 24 Subtotal 166 237 Project-level contingency (12%) 28 Total estimated hours 265 hours Calendar timeline: With one developer working roughly 25 productive hours per week on this project, plus 3 client feedback rounds (approximately 10 business days total), the realistic delivery window is 12 to 14 weeks. Presenting the Estimate to the Client How you communicate the estimate matters as much as the numbers. Our recommendations: Present a range, not a single number (for example 250 to 290 hours). Show the assumptions your estimate is based on. If those assumptions change, the estimate changes. Split the price by phase so the client sees where the value is. Clearly separate fixed scope from hourly work (like ongoing content updates). Include a change request process in your proposal. Common Mistakes to Avoid Estimating in isolation: Involve the developers who will actually do the work. Forgetting non-coding time: Meetings, emails, documentation, and deployment all count. Not versioning your estimate: Keep a copy of the original scope. When it changes, issue a revised estimate. Confusing effort with duration: 100 hours of work does not equal 100 hours on the calendar. No post-project review: Compare estimated vs actual hours after every project. This is how you get better. Tools We Recommend in 2026 Notion or Google Sheets for the estimation matrix Toggl or Clockify for tracking actual

How to Estimate Web Development Project Timelines: A Practical Framework for Agencies and Freelancers Read More »