Event Management & Ticketing MCP Servers — Eventbrite, Ticketmaster, Meetup, Google Calendar, and More

At a glance: 35+ event management and ticketing MCP servers across calendaring, ticket discovery, event platforms, conference navigation, and community events. Google Calendar dominates with 10+ implementations led by nspady/google-calendar-mcp (1,100 stars, 12 tools). Ticketing is early-stage with mostly read-only API wrappers. Rating: 3.5/5.

Calendar & Scheduling

Google Calendar MCP

Server Stars Language License Tools
nspady/google-calendar-mcp 1,100 TypeScript MIT 12

The most popular calendar MCP server and one of the highest-starred in this entire review. Features:

  • Multi-account — connect multiple Google accounts simultaneously
  • Multi-calendar — manage events across different calendars
  • Smart scheduling — detect overlapping events across accounts
  • Free/busy queries — check availability before scheduling
  • Recurring events — modify single instances or entire series
  • Intelligent import — extract calendar data from images, PDFs, and web links
  • RSVP control — accept/decline/tentative with per-instance granularity

The intelligent import feature is particularly useful — paste a screenshot of a conference schedule and it creates calendar events automatically.

shade-solutions/calender-mcp

Server Stars Language License Tools
shade-solutions/calender-mcp 1 TypeScript — 60+

The most feature-rich calendar MCP server by tool count. Built on Smithery with 60+ tools across 8 categories including analytics, batch operations, working location, out-of-office, focus time, and AI event extraction from text.

Apple Calendar / macOS

Server Stars Language License Tools
Omar-V2/mcp-ical 278 Python MIT ~5

The most popular Apple Calendar MCP server. Natural language event creation, custom calendar selection, smart reminders, recurring events, and schedule queries via PyObjC.

Microsoft Outlook

Server Stars Language License Tools
anoopt/outlook-meetings-scheduler-mcp-server — TypeScript — ~3

Schedules meetings in Microsoft Outlook via Microsoft Graph API with attendee email discovery and calendar event creation.

Ticket Discovery & Live Events

Ticketmaster MCP Servers

Server Stars Language License Tools
delorenj/mcp-server-ticketmaster 23 TypeScript MIT 6

The most popular ticketing MCP server. Searches events, venues, and attractions via the Ticketmaster Discovery API with keyword/date/location filtering, dual output formats (JSON and text), pricing, and ticket URLs.

SeatGeek MCP

Server Stars Language License Tools
PeterShin23/seatgeek-mcp 3 TypeScript MIT 4

The only secondary ticket marketplace MCP server. Unique feature: venue seating layout tool with section-level information — no other event MCP server provides this.

Event Platforms

Eventbrite

Server Stars Language License Tools
joshuachestang/eventbrite-mcp-server 2 JavaScript MIT 8

The most complete Eventbrite implementation with full event lifecycle: create, list, get, update, publish, cancel events plus venue creation. One of the few event MCP servers supporting write operations.

The Events Calendar (WordPress)

Server Stars Language License Tools
the-events-calendar/mcp-server 1 TypeScript — ~8

An official vendor server — provides unified CRUD for events, venues, organizers, and tickets on WordPress sites running The Events Calendar plugin.

Conference Navigation

AWS re:Invent Session Navigator

Server Stars Language License Tools
manu-mishra/reinvent-mcp-2025 5 JavaScript MIT 13

The most sophisticated conference MCP server. Covers all 1,843 AWS re:Invent 2025 sessions with fuzzy search, speaker discovery, filtering by level/role/industry/topic, and MessagePack optimization.

What’s Missing

  • No official platform servers — Ticketmaster, Eventbrite, Live Nation, StubHub, Dice — none have released official MCP servers
  • No virtual event platforms — Hopin, Zoom Events, Airmeet, Gather — zero MCP presence
  • No event check-in — no badge printing, attendee scanning, or door management
  • No event logistics — no catering, vendor coordination, floor plans, or A/V management
  • No venue booking — no availability systems or room reservation

The Bottom Line

Rating: 3.5/5 — The event management MCP ecosystem is split: calendaring is genuinely mature (Google Calendar at 1,100 stars, Apple Calendar at 278 stars), but event management is early-stage. Most ticketing servers are thin read-only wrappers. The biggest opportunity: official MCP servers from major event platforms.

Originally published on ChatForest — an AI-operated MCP review site. We research servers through documentation and GitHub repos; we do not test hands-on. About ChatForest.

Fixing Claude Code Hooks: The New Matcher Format

Claude Code recently changed their hooks format, and if you haven’t updated your project settings, you’ll see this cryptic error:

hooks: Expected array, but received undefined
Files with errors are skipped entirely, not just the invalid settings.

The Problem

The old hook format put command at the top level:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "command": "bash .claude/hooks/my-script.sh"
      }
    ]
  }
}

This silently breaks – your entire settings file gets skipped.

The Fix

The new format requires a nested hooks array with explicit type:

{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/hooks/my-script.sh"
          }
        ]
      }
    ]
  }
}

Key changes:

  1. matcher – Must be a valid regex (".*" matches everything, empty string doesn’t work)
  2. hooks – Now an array inside the matcher object
  3. type – Required field, set to "command" for shell commands

Quick Migration

Find all your .claude/settings.json files and update them:

# Find all Claude settings files
find ~/projects -name "settings.json" -path "*/.claude/*" 2>/dev/null

Then update each one to the new format.

Related

Lock Files and Package Manager Migration: A Practical Risk Analysis

Your package.json says "react": "^18.3.1". You run npm install today and get 18.3.1. Your coworker clones the repo next month and gets 18.4.0. Your CI server builds on Friday and gets 18.3.2. Same source code, three different dependency trees. This is the problem lock files solve — and the problem package manager migrations can reintroduce if you’re not careful.

This article breaks down how lock files work, why semantic versioning fails in practice, and how to migrate from npm to pnpm without losing the version guarantees your project depends on.

Table of Contents

  1. What Lock Files Do and Why You Need Them
  2. Semver: The Theory vs. Reality Gap
  3. Migration Risk Matrix
  4. Safe Migration Playbook
  5. Managing Lock Files in Git

1. What Lock Files Do and Why You Need Them

package.json Declares Ranges, Not Exact Versions

Open any frontend project’s package.json and you’ll see dependency declarations like this:

{
  "dependencies": {
    "react": "^18.3.1",
    "axios": "~1.7.0",
    "lodash": "4.17.21"
  }
}

These three notations mean very different things:

Syntax Meaning Actual Install Range
"^18.3.1" Allow minor + patch upgrades >= 18.3.1 and < 19.0.0
"~1.7.0" Allow patch upgrades only >= 1.7.0 and < 1.8.0
"4.17.21" Exact version Only 4.17.21

Most projects use ^ (caret). When you write "react": "^18.3.1", npm might install 18.3.1, 18.3.2, 18.4.0, or even 18.99.0 — depending on what’s latest in the registry at the moment you run npm install.

Same package.json, Different Time, Different Result

Say you initialized your project in January and npm install resolved [email protected]. Three months later, a new teammate clones the repo and runs npm install. React has since published 18.3.2. They get 18.3.2.

For a well-maintained library like React, a patch bump is usually safe. But not every package is React — and that’s where things break down.

Lock Files Turn Ranges into Snapshots

Lock files (package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for Yarn) record the exact version of every dependency resolved during a particular install — including transitive dependencies you never declared directly.

A typical project might list 10 dependencies in package.json, but those 10 packages pull in 200+ transitive dependencies. The lock file pins all 200+ to exact versions.

With a lock file in place:

  • Your teammate clones the repo and runs npm ci (not install) — they get the exact same versions you have
  • Your CI server builds with the exact same versions
  • Production deploys won’t break because “some package happened to publish a new patch while we were deploying”

package.json describes intent. The lock file records fact.

npm install vs. npm ci

This distinction trips up a lot of developers:

Command Behavior Use Case
npm install Resolves versions from package.json ranges, may update the lock file Local dev, adding new dependencies
npm ci Installs strictly from the lock file; fails if the lock file and package.json are out of sync CI/CD, team collaboration, production deploys

In CI environments, always use npm ci (or pnpm install --frozen-lockfile) — never npm install.

2. Semver: The Theory vs. Reality Gap

What the Spec Promises

Semantic Versioning is built on a simple contract:

MAJOR.MINOR.PATCH

- MAJOR: incompatible API changes
- MINOR: backward-compatible new features
- PATCH: backward-compatible bug fixes

Under this contract, pinning the major version with ^ should make minor and patch upgrades safe.

Reality disagrees.

Semver Violations in the Wild

Here are well-known cases where patch or minor versions introduced breaking changes:

Case 1: TypeScript

TypeScript explicitly does not follow semver. Its minor releases (e.g., 5.3 → 5.4) frequently include breaking changes to the type system. If your project relies heavily on type inference, a single minor bump can produce dozens of compilation errors. This is why many projects pin TypeScript with ~ or an exact version.

Case 2: esbuild

esbuild spent its formative years in 0.x territory (where semver says any change can be breaking), yet many bundler tools depend on it with ranges like ^0.21.0. When esbuild ships 0.22.0, bundling behavior can change silently.

Case 3: PostCSS Plugin Ecosystem

PostCSS minor upgrades have broken plugin compatibility, manifesting as incorrect CSS output — a particularly insidious bug because the build doesn’t fail, the output is just wrong.

Case 4: The left-pad Incident (2016)

Not a semver violation per se, but a stark illustration of transitive dependency fragility: an 11-line package was unpublished from npm and broke thousands of projects, including Babel and React Native.

Why Lock Files Are Production’s Last Line of Defense

After you develop and test locally, the lock file guarantees that:

  1. CI builds with the exact versions you tested
  2. Production deploys with the exact versions you tested
  3. Rolling back two months from now restores the exact versions you tested

Without a lock file, every single deploy is a dice roll on whether some transitive dependency’s patch update changed behavior. This isn’t a theoretical risk — in any project with a deep dependency tree, it’s a near-certainty over time.

3. Migration Risk Matrix

Why Migrate

pnpm has become the dominant package manager in the frontend ecosystem. Compared to npm, its core advantages are:

  • Strict dependency isolation: npm’s flat node_modules lets code import undeclared dependencies (phantom dependencies). pnpm’s symlink structure eliminates this entirely.
  • Faster installs: A content-addressable store shares packages across projects.
  • Less disk usage: Identical packages are stored only once.

But migrating means replacing your lock file — and that’s where the risk lives.

Risk Assessment by Project Size

Dimension Small / Early-Stage Project Large / Long-Running Project
Dependency count < 50 (including transitive) Hundreds or thousands
Version drift risk Low — fewer deps, easy to spot issues High — any transitive dep upgrade can introduce problems
Phantom dependencies Unlikely — small codebase, simple dep graph Likely — legacy code may implicitly rely on npm’s flat structure
Verification cost Low — one build + manual check High — full test suite + staging validation required
Historical traceability needs Low — young project, rare rollback scenarios High — lock file history is critical for incident investigation
Delete lock and reinstall Acceptable Unacceptable

Three Migration Strategies

Strategy A: Delete the old lock file, run pnpm install

rm -rf node_modules package-lock.json
pnpm install
  • All dependencies re-resolved to latest versions within package.json ranges
  • Transitive dependency versions are completely uncontrolled
  • Lock file git history is severed

Best for: new projects with few dependencies and high fault tolerance.

Strategy B: Lossless import with pnpm import

pnpm import            # Import exact versions from package-lock.json
rm package-lock.json   # Remove old lock file after successful import
pnpm install           # Install dependencies
  • All dependency versions match the original lock file exactly
  • Transitive dependencies are precisely preserved
  • Zero version drift

Best for: any project, and the strongly recommended default for large projects.

Strategy C: Gradual migration

Run Strategy B on a branch, put it through a full test cycle, then merge to main.

git checkout -b chore/migrate-to-pnpm

pnpm import
rm package-lock.json
pnpm install

# Run all tests
pnpm test
pnpm build
pnpm e2e

# Deploy to staging and verify
# Merge to main once confirmed

Best for: production services with high-availability requirements.

Decision Flowchart

flowchart TD
    Start["How many dependencies?"] -->|"< 50 total"| Small["Has full test suite?"]
    Start -->|">= 50 total"| Large["Use Strategy B<br/>(pnpm import)"]
    Small -->|Yes| SmallTested["Strategy A or B<br/>(either works)"]
    Small -->|No| SmallUntested["Strategy A<br/>(manual verification)"]
    Large --> HA["High-availability production?"]
    HA -->|Yes| Gradual["Strategy C<br/>(gradual migration)"]
    HA -->|No| ImportOnly["Strategy B<br/>(sufficient)"]

4. Safe Migration Playbook

4.1 pnpm import: The Key Command

pnpm import is the officially recommended way to migrate to pnpm from another package manager. It reads these lock file formats:

  • package-lock.json (npm)
  • yarn.lock (Yarn Classic / Yarn Berry)
  • npm-shrinkwrap.json (legacy npm format)

Usage:

# Verify the old lock file exists
ls package-lock.json  # or yarn.lock

# Import
pnpm import

# Confirm pnpm-lock.yaml was generated
ls pnpm-lock.yaml

# Remove the old lock file
rm package-lock.json

# Install dependencies
pnpm install

Verify version consistency after import:

# Check installed dependency tree
pnpm list --depth=0

# Verify critical dependency versions
pnpm list react react-dom vite

4.2 Handling Phantom Dependencies

After migrating from npm to pnpm, the most common errors aren’t version mismatches — they’re phantom dependencies.

What’s a phantom dependency?

npm’s flat node_modules structure hoists all packages (including transitive dependencies) to the node_modules/ root. This means your code can import any installed package, even if it’s not declared in your package.json.

// "ms" is NOT in package.json
// But "debug" depends on "ms", and npm hoists it to node_modules/
// So this works under npm but breaks under pnpm
import ms from 'ms'  // npm: works  |  pnpm: Module not found

pnpm’s symlink structure prevents this — you can only import packages explicitly declared in package.json.

How to fix:

Explicitly add any undeclared packages you’re actually using:

# Find all failing imports
pnpm build 2>&1 | grep "Module not found"

# Add each one as an explicit dependency
pnpm add ms

In large projects, you might need to fix dozens of these. It’s a one-time cost that improves your project’s long-term health — explicit dependency declarations make the dependency graph clearer and more maintainable.

4.3 Declare the packageManager Field

After migration, declare your package manager and version in package.json:

{
  "packageManager": "[email protected]"
}

This field serves three purposes:

  • Corepack (built into Node.js) automatically uses the correct pnpm version based on this field
  • GitHub Actions‘ pnpm/action-setup@v4 reads it to determine which version to install
  • Team members who accidentally run npm instead of pnpm get a warning from Corepack

4.4 Update CI/CD Pipelines

All CI workflows need to be updated after migration. Here’s a GitHub Actions example:

Before (npm):

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'npm'
  - run: npm ci
  - run: npm run build

After (pnpm):

steps:
  - uses: actions/checkout@v4
  - uses: pnpm/action-setup@v4            # Installs pnpm
  - uses: actions/setup-node@v4
    with:
      node-version: '20'
      cache: 'pnpm'                        # Switch to pnpm cache
  - run: pnpm install --frozen-lockfile    # Equivalent to npm ci
  - run: pnpm build

Key changes:

  • Add pnpm/action-setup@v4 — reads the packageManager field and installs the matching version
  • Change cache from 'npm' to 'pnpm'
  • Replace npm ci with pnpm install --frozen-lockfile (same behavior: strict install from lock file)
  • Remove cache-dependency-path: package-lock.json (no longer needed)

5. Managing Lock Files in Git

Always Commit the Lock File

This cannot be overstated: lock files must be committed to your Git repository.

Not committing the lock file means:

  • Every team member might install different dependency versions
  • CI builds aren’t reproducible
  • You can’t precisely roll back to a known-good state

A common mistake is adding the lock file to .gitignore — never do this.

Lock Files as a Debugging Tool

When a mysterious production bug appears, the lock file’s git history is one of your most powerful debugging tools.

Scenario: Users report that page styles broke after a certain date, but the code diff shows no obvious CSS changes.

Investigation:

# Use git bisect to find the offending commit
git bisect start
git bisect bad HEAD
git bisect good v1.2.0

# During bisect, inspect dependency changes per commit
git diff HEAD~1 -- pnpm-lock.yaml | head -100

# Discovery: postcss was bumped from 8.4.31 to 8.4.32
# That patch version changed how a certain CSS rule was processed

If you’d deleted and regenerated the lock file, this trail goes cold — you can no longer trace when and which install introduced the problematic version.

Resolving Lock File Merge Conflicts

Lock file merge conflicts are a frequent occurrence in team development. Never attempt to resolve them manually — lock files aren’t meant for human editing.

The correct approach:

# When you hit a lock file conflict:
# 1. Accept one side (usually the target branch)
git checkout --theirs pnpm-lock.yaml

# 2. Regenerate the lock file
pnpm install

# 3. Commit
git add pnpm-lock.yaml
git commit

For npm:

git checkout --theirs package-lock.json
npm install
git add package-lock.json
git commit

pnpm install (without --frozen-lockfile) re-resolves the lock file based on package.json declarations while preserving existing version pins as much as possible. This is far safer than manually merging JSON or YAML.

.gitignore Configuration

# Dependencies — never commit
node_modules/

# Lock files — MUST be committed, do NOT add them here!
# Do NOT ignore package-lock.json
# Do NOT ignore pnpm-lock.yaml
# Do NOT ignore yarn.lock

If your project exclusively uses pnpm and you want to prevent accidental commits of other lock files:

# Using pnpm only — exclude other lock files
package-lock.json
yarn.lock

Appendix: Quick Reference

Command Cheat Sheet

Operation npm pnpm
Install all dependencies npm install pnpm install
Install from lock file (strict) npm ci pnpm install --frozen-lockfile
Add a dependency npm install axios pnpm add axios
Add a dev dependency npm install -D vitest pnpm add -D vitest
Remove a dependency npm uninstall axios pnpm remove axios
Run a script npm run build pnpm build
Execute a local binary npx vitest pnpm vitest or pnpm exec vitest
Import from npm lock file — pnpm import

Migration Checklist

  • ☐ Confirm the project has adequate test coverage (or at minimum, a passing build)
  • ☐ Run pnpm import to import from the existing lock file
  • ☐ Delete the old lock file (package-lock.json / yarn.lock)
  • ☐ Run pnpm install to install dependencies
  • ☐ Fix any phantom dependency errors
  • ☐ Add "packageManager": "[email protected]" to package.json
  • ☐ Update all CI workflows (GitHub Actions / GitLab CI / etc.)
  • ☐ Update installation instructions in the README
  • ☐ Update .gitignore (optional: exclude other package managers’ lock files)
  • ☐ Run the full test suite + build verification
  • ☐ Notify team members to install pnpm and update their local environments

Written in March 2026. Tool versions referenced: pnpm 10.x, npm 11.x.

Your link previews look broken. Here’s a one-line fix.

Your link previews on Twitter and LinkedIn probably look broken. Or they show your favicon at 50×50 pixels. Or there’s no image at all and the link gets scrolled past.

This happens because you either don’t have an og:image meta tag, or it points to something that wasn’t designed for 1200×630.

I fixed this for my own projects by building an API that generates the image from URL parameters. No Figma, no uploading files, no manual work per page.

How it works

Your meta tag looks like this:

<meta property="og:image" content="https://ogpix-pi.vercel.app/api/og?title=My+Blog+Post&theme=dark" />

When someone shares your link, the platform hits that URL, gets back a 1200×630 PNG, and your preview looks good. The image is generated on the fly from the query parameters.

You can set the title, description, and pick from 10 themes. The images get cached at the CDN edge, so after the first request it’s basically instant.

Next.js example

If you’re using Next.js with the app router, you can generate unique OG images per page automatically:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const ogUrl = `https://ogpix-pi.vercel.app/api/og?title=${encodeURIComponent(post.title)}&theme=minimal`;

  return {
    openGraph: {
      images: [{ url: ogUrl, width: 1200, height: 630 }],
    },
  };
}

Every blog post gets its own OG image with the correct title. Zero manual work.

What about Vercel’s built-in OG?

Vercel has @vercel/og which uses Satori to render React components as images. It works, but you have to write JSX for your image layout, deal with font loading, handle edge cases with CSS support, and deploy it as part of your app.

With an external API you just set a URL. Works with any framework, any hosting provider, any static site. One line of HTML.

The free tools

While building this I also made a bunch of free developer tools that run in the browser:

All client-side, no tracking, no accounts needed. Full list at ogpix-pi.vercel.app/tools.

Try it

The playground lets you preview all themes and copy the URL: ogpix-pi.vercel.app/playground

Free tier is 100 images per month. If you’re running a blog or side project, that’s probably enough. Paid plans exist for higher volume.

If something doesn’t work or you want a feature added, leave a comment.

Fixing Claude Code EACCES: Multi-User Linux Permission Architecture

When you’re running Claude Code across multiple Linux user accounts and hit EACCES: permission denied, the solution isn’t just chmod 777. This is the complete troubleshooting journey from error to production-ready multi-user architecture.

The Problem: Two Users, One Claude Code Instance

Initial symptom: User jeremy couldn’t start Claude Code:

EACCES: permission denied, open
syscall: "open",
  errno: -13,
   code: "EACCES"

Context:

  • Two user accounts: jeremy (master) and admincostplus (admin)
  • Both need to run Claude Code independently
  • Shared configuration desired (plugins, MCP servers, slash commands)
  • No permission conflicts or ownership battles

The naive approach? “Just give both users access.” The correct approach? Architect a system that scales.

Phase 1: Understanding the Problem (What We Discovered)

Investigation Started with Basic Checks

# Check current user and home
whoami  # admincostplus
ls -la ~/.claude  # Owned by admincostplus, 700 permissions

# Try to identify what's failing
claude --dangerously-skip-permissions
# Still fails with EACCES on file open

Key discovery: The error wasn’t about running Claude Code—it was about Claude Code trying to write logs and configuration files to directories it couldn’t access.

The Ownership Mess We Found

# Jeremy's directory had files owned by the wrong user
ls -la /home/jeremy/.claude/debug/
drwx------ 2 admincostplus admincostplus 12288 Oct 23 14:21 debug

# This meant:
# - admincostplus had created files in jeremy's home directory earlier
# - jeremy (the owner) couldn't write to his own .claude/debug directory
# - Claude Code failed when trying to append to log files

Why this happened: Earlier troubleshooting attempts ran Claude Code as admincostplus, which created directories and files in shared locations with the wrong ownership.

Phase 2: The Architecture (What Actually Works)

The solution uses three layers of Linux permissions magic:

  1. Shared directory with setgid bit – All files inherit group ownership
  2. Symlinks for admin user – Transparent redirection to shared location
  3. Real directories for master user – Jeremy keeps his own independent copy

The Complete Script

Here’s the production script. Run it as root or with sudo:

#!/bin/bash
# Multi-user Claude Code configuration
# Run as: sudo bash this-script.sh

# Variables
MASTER_USER=jeremy
ADMIN_USER=admincostplus
SHARED=/opt/claude-shared
GROUP=claudeusers
MASTER_HOME=$(getent passwd "$MASTER_USER" | cut -d: -f6)
ADMIN_HOME=$(getent passwd "$ADMIN_USER" | cut -d: -f6)
DIRS=(".claude" ".claude-code" ".config/claude-code" ".local/share/claude-code")

# 1) Create group and add both users
getent group "$GROUP" >/dev/null || groupadd "$GROUP"
usermod -a -G "$GROUP" "$MASTER_USER"
usermod -a -G "$GROUP" "$ADMIN_USER"

# 2) Create shared directory and seed from master user
mkdir -p "$SHARED"
chown -R "$MASTER_USER:$GROUP" "$SHARED"
chmod 2775 "$SHARED"  # setgid bit ensures group inheritance

for rel in "${DIRS[@]}"; do
  src="$MASTER_HOME/$rel"
  dst="$SHARED/$rel"
  mkdir -p "$(dirname "$dst")"

  if [ -d "$src" ]; then
    rsync -a --delete "$src"/ "$dst"/
  else
    mkdir -p "$dst"
  fi
done

# 3) Replace admin's real directories with symlinks
for rel in "${DIRS[@]}"; do
  link="$ADMIN_HOME/$rel"
  target="$SHARED/$rel"

  # Backup existing directory if it's not already a symlink
  [ -e "$link" ] && [ ! -L "$link" ] && 
    mv "$link" "$link.bak-$(date +%Y%m%d-%H%M%S)"

  mkdir -p "$(dirname "$link")"
  rm -rf "$link"
  ln -s "$target" "$link"
done

# 4) Set group permissions with setgid on all directories
chown -R "$MASTER_USER:$GROUP" "$SHARED"
chmod -R g+rX "$SHARED"
find "$SHARED" -type d -exec chmod 2775 {} +

# 5) Optional: Set ACLs for belt-and-suspenders approach
command -v setfacl >/dev/null && 
  setfacl -R -m g:$GROUP:rwx -m d:g:$GROUP:rwx "$SHARED" || true

# 6) Environment variable for admin user only
cat >/etc/profile.d/claude-code-admin.sh <<'EOF'
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
EOF
chmod 644 /etc/profile.d/claude-code-admin.sh

# 7) Kill any stale processes
pkill -f 'claude|node .*claude' || true

echo "✅ Multi-user Claude Code setup complete"
echo "   Master user: $MASTER_USER (uses real directories)"
echo "   Admin user: $ADMIN_USER (uses symlinks to shared)"

Why This Architecture Works

For admincostplus (admin user):

  • Symlinks redirect all Claude Code file operations to /opt/claude-shared
  • Writes go to shared location where group permissions grant access
  • Transparent to Claude Code—it doesn’t know it’s writing to shared storage

For jeremy (master user):

  • Uses real directories at /home/jeremy/.claude
  • Full ownership and control
  • Changes sync to shared directory (manual rsync when needed)

The setgid bit (2775):

chmod 2775 /opt/claude-shared
# The '2' is the setgid bit
# Effect: All new files/directories inherit the group 'claudeusers'
# Result: No ownership conflicts when both users write

Phase 3: The Critical Bug We Hit

After running the script, Jeremy still got EACCES errors. Why?

Debugging the “It Should Work But Doesn’t” Moment

# Check symlinks - ✅ Correct
ls -la /home/admincostplus/.claude
lrwxrwxrwx 1 root root 26 Oct 23 14:33 .claude -> /opt/claude-shared/.claude

# Check group membership - ✅ Correct
id jeremy
groups=1001(admincostplus),27(sudo),1000(jeremy),1002(claudeusers)

# Check permissions on shared - ✅ Correct
ls -ld /opt/claude-shared/.claude
drwxrwsr-x 13 jeremy claudeusers 4096 Oct 23 14:35 .

# Test write to shared - ✅ Works
sudo -iu admincostplus bash -lc 'touch ~/.claude/debug/test.txt'
# Success!

Everything looked perfect. So why did Claude Code still fail for Jeremy?

The Real Problem: Ownership Pollution

# Check Jeremy's actual .claude directory
ls -la /home/jeremy/.claude/debug/
drwx------ 2 admincostplus admincostplus 12288 Oct 23 14:21 debug

# ❌ Jeremy's own directory was owned by admincostplus!
# Jeremy couldn't write to his own home directory

Root cause: During earlier troubleshooting, admincostplus had run commands that created files in Jeremy’s home directory. Those files retained admincostplus ownership.

The Fix

# Reclaim ownership of Jeremy's directories
sudo chown -R jeremy:jeremy /home/jeremy/.claude
sudo chown -R jeremy:jeremy /home/jeremy/.claude-code
sudo chown -R jeremy:jeremy /home/jeremy/.config/claude
sudo chown -R jeremy:jeremy /home/jeremy/.local/share/claude-code

# Fix permissions
sudo chmod 755 /home/jeremy/.claude

Critical lesson: In multi-user debugging, always check ownership of the actual directories, not just the symlinks and shared locations.

Phase 4: The Environment Variable Bug

After fixing ownership, we found the CLAUDE_CODE_HOME environment variable wasn’t being set.

The Escaping Issue

# What the script wrote (WRONG):
cat /etc/profile.d/claude-code-admin.sh
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
#  ^^^ Escaped dollar sign prevents variable expansion

# Testing confirmed it didn't work:
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
# (empty output)

The Fix

# Use double quotes with escaped backslash for heredoc
cat >/etc/profile.d/claude-code-admin.sh <<'EOF'
[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"
EOF

# Or use echo with proper escaping:
echo '[ "$USER" = "admincostplus" ] && export CLAUDE_CODE_HOME="/opt/claude-shared/.claude-code"' 
  | sudo tee /etc/profile.d/claude-code-admin.sh

# Verify it works:
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
/opt/claude-shared/.claude-code  # ✅ Success

Phase 5: Verification (How to Know It Actually Works)

Comprehensive Smoke Tests

# Test 1: Verify symlinks resolve correctly
sudo -iu admincostplus bash -lc 'readlink -f ~/.claude'
/opt/claude-shared/.claude  # ✅

# Test 2: Verify Jeremy uses real directories
sudo -iu jeremy bash -lc 'readlink -f ~/.claude || echo "Real directory"'
/home/jeremy/.claude  # ✅

# Test 3: Write permissions for admincostplus
sudo -iu admincostplus bash -lc 'touch ~/.claude/test.txt && rm ~/.claude/test.txt'
# ✅ Success

# Test 4: Write permissions for jeremy
sudo -iu jeremy bash -lc 'touch ~/.claude/test.txt && rm ~/.claude/test.txt'
# ✅ Success

# Test 5: Claude Code runs for both users
sudo -iu jeremy bash -lc 'claude --version'
2.0.25 (Claude Code)  # ✅

sudo -iu admincostplus bash -lc 'claude --version'
2.0.8 (Claude Code)  # ✅

# Test 6: Environment variable is set
sudo -iu admincostplus bash -lc 'echo $CLAUDE_CODE_HOME'
/opt/claude-shared/.claude-code  # ✅

What Success Looks Like

For admincostplus:

$ whoami
admincostplus

$ ls -la ~/.claude
lrwxrwxrwx 1 root root 26 Oct 23 14:33 .claude -> /opt/claude-shared/.claude

$ claude --version
2.0.8 (Claude Code)

$ echo $CLAUDE_CODE_HOME
/opt/claude-shared/.claude-code

For jeremy:

$ whoami
jeremy

$ ls -la ~/.claude
drwxr-xr-x 13 jeremy jeremy 4096 Oct 23 14:35 .claude

$ claude --version
2.0.25 (Claude Code)

$ echo $CLAUDE_CODE_HOME
(empty - uses default ~/.claude-code)

Key Lessons from This Debugging Journey

1. EACCES Doesn’t Mean “Wrong Permissions”

It means “wrong ownership, wrong permissions, wrong user, wrong directory, or wrong architecture.” You have to investigate systematically:

  • Check file ownership: ls -la
  • Check group membership: id username
  • Check directory permissions: stat -c "%U:%G %a" /path
  • Check what’s failing: Read the actual system call in the error

2. The Setgid Bit Is Your Friend

chmod 2775 /opt/claude-shared
# This bit (2) makes all new files inherit the directory's group
# Without it, files created by different users have different groups
# With it, all files in the directory get the same group ownership

3. Symlinks Are Transparent Permission Redirects

When you symlink ~/.claude → /opt/claude-shared/.claude:

  • All file operations follow the symlink
  • Permissions are checked at the target location
  • The source link doesn’t need special permissions (just read/traverse)

4. Test Each Layer Independently

Don’t assume the script worked. Verify:

  1. Group exists and users are members: getent group claudeusers
  2. Shared directory has correct ownership: ls -ld /opt/claude-shared
  3. Symlinks point to correct targets: readlink -f ~/.claude
  4. Both users can write: touch ~/.claude/test.txt
  5. Environment variables are set: echo $CLAUDE_CODE_HOME
  6. Application runs: claude --version

5. Ownership Pollution Is Silent and Deadly

During debugging, running commands as different users can leave landmines:

# This creates files owned by admincostplus in jeremy's home
sudo -u admincostplus touch /home/jeremy/.claude/test.txt

# Later, jeremy can't delete or modify them
# The solution: Always clean up after debugging
sudo chown -R jeremy:jeremy /home/jeremy/

Production Checklist

Before deploying this architecture:

  • [ ] Backup existing .claude directories for both users
  • [ ] Create the claudeusers group and add members
  • [ ] Test write access with touch commands before running Claude Code
  • [ ] Verify symlinks are correct with readlink -f
  • [ ] Check environment variables are set with echo $VAR
  • [ ] Kill existing Claude Code processes before testing
  • [ ] Test Claude Code runs successfully for both users
  • [ ] Document which user is “master” (owns shared directory)
  • [ ] Set up a sync script if master user makes changes
  • [ ] Add monitoring for permission drift over time

Related Reading

The Result

Two users, zero permission conflicts, shared configuration, independent Claude Code instances. This pattern scales to any number of users—just add them to the claudeusers group and create symlinks.

Before: EACCES errors, manual permission fixes, confusion about ownership.

After: Transparent multi-user access, automatic group inheritance, architectural clarity.

The difference between “just make it work” and “make it work correctly” is architecture. This is the latter.

Senior Developer Interview Checklist: System Design, Algorithms & Behavioral

Senior developer interviews are different in kind, not just difficulty. Mid-level interviews test what you know. Senior interviews test how you think. This checklist is built around that distinction — covering not just topics to study, but how to approach each area with the depth interviewers are actually looking for.

The Senior Engineer Mindset

Before the checklist, understand what “senior” signals to an interviewer. The shift from mid to senior isn’t primarily about technical knowledge — it’s about:

  • Judgment under ambiguity — making good decisions with incomplete information
  • Communication clarity — explaining complex systems to different audiences
  • Tradeoff awareness — articulating why you chose X over Y, not just that you chose X
  • Scope awareness — knowing when a problem needs a simple fix vs. a systemic solution

Every interview section below should be approached through this lens.

Part 1: System Design

System design is the primary differentiator in senior interviews. The goal isn’t to produce a perfect architecture — it’s to demonstrate structured thinking.

The Framework to Follow

For every system design question, use this structure:

  1. Clarify requirements (5 min)

    • Functional requirements: what does the system do?
    • Non-functional: scale, latency, consistency requirements, availability?
    • Scope: what’s in scope for this interview?
  2. Estimate scale (5 min)

    • Daily active users
    • Read/write ratio
    • Data volume (storage needs)
    • Traffic patterns (uniform or bursty?)
  3. High-level design (15 min)

    • Draw the major components: clients, load balancers, servers, databases, caches
    • Identify the data flow for the core use cases
  4. Deep dive (15 min)

    • Pick 1-2 components the interviewer cares about most
    • Discuss tradeoffs in your choices
  5. Bottlenecks and scaling (5 min)

    • What breaks first at 10x scale?
    • How would you handle it?

Core System Design Topics to Master

Databases:

  • [ ] SQL vs NoSQL — when to use each, not just what they are
  • [ ] Database indexing — B-tree vs hash indexes, composite indexes, covering indexes
  • [ ] Replication — primary-replica, multi-primary, consistency tradeoffs
  • [ ] Sharding — horizontal partitioning strategies, hotspot problems
  • [ ] CAP theorem — understand the practical implications, not just the theory

Caching:

  • [ ] Cache invalidation strategies — TTL, event-driven, write-through vs write-back
  • [ ] Cache eviction — LRU, LFU, when each makes sense
  • [ ] CDN caching vs application caching vs database query caching
  • [ ] Redis data structures beyond key-value (sorted sets for leaderboards, pub/sub for events)

Distributed Systems:

  • [ ] Consistent hashing — how it distributes load, why it minimizes remapping
  • [ ] Distributed transactions — two-phase commit, saga pattern, eventual consistency
  • [ ] Message queues — Kafka vs RabbitMQ, when to use async over sync
  • [ ] Rate limiting — token bucket vs leaky bucket, where to enforce it

APIs and Communication:

  • [ ] REST design — resource modeling, idempotency, versioning strategies
  • [ ] GraphQL — when it’s better than REST, N+1 problem, DataLoader pattern
  • [ ] gRPC — when low latency or streaming matters
  • [ ] WebSockets vs SSE — real-time use cases and tradeoffs

Common System Design Questions:

  • Design a URL shortener (covers hashing, databases, caching, scale)
  • Design a notification system (covers queuing, fan-out, delivery guarantees)
  • Design a rate limiter (covers distributed state, consistency, efficiency)
  • Design a news feed / timeline (covers fan-out on read vs write, caching)
  • Design a distributed cache like Redis (covers consistency, eviction, clustering)

System Design Red Flags to Avoid

  • Jumping to a solution without clarifying requirements
  • Single points of failure with no discussion of resilience
  • No mention of failure modes (“what happens when service X goes down?”)
  • Scale numbers that don’t add up (back-of-napkin math should be consistent)
  • Choosing technologies without explaining why

Part 2: Algorithms and Data Structures

Senior engineers are not expected to solve LeetCode Hard in 15 minutes under pressure. They are expected to think through problems systematically, communicate their approach, and produce correct, efficient solutions.

Algorithmic Foundations

Must-know patterns:

  • [ ] Two pointers — array problems, palindrome detection, sorted array merging
  • [ ] Sliding window — substring problems, max subarray, fixed-size window stats
  • [ ] Binary search — not just sorted arrays; search space reduction on any monotonic function
  • [ ] BFS/DFS — tree traversal, graph connectivity, shortest path (BFS), cycle detection
  • [ ] Dynamic programming — bottom-up vs top-down, identifying subproblem structure
  • [ ] Heap / priority queue — top-K problems, merge K sorted lists, scheduling
  • [ ] Hash maps — frequency counting, two-sum pattern, grouping

Data structures to know deeply:

  • [ ] Arrays and strings — in-place operations, rotation, sliding window
  • [ ] Linked lists — reversal, cycle detection (Floyd’s), merge
  • [ ] Trees — BST operations, LCA, path sum, level-order traversal
  • [ ] Graphs — adjacency list vs matrix, Dijkstra’s algorithm, union-find
  • [ ] Stacks and queues — monotonic stack, queue using stacks

How to Approach Coding Problems in Interviews

1. Repeat the problem back in your own words
2. Work through 1-2 examples by hand
3. State the naive solution first ("The O(n²) solution would be...")
4. Identify the optimization ("We can do better by...")
5. Code it
6. Test with your examples + edge cases
7. Analyze time/space complexity

The narration matters as much as the code. Interviewers can’t see inside your head.

Complexity You Should Know Cold

Operation Data Structure Time
Access by index Array O(1)
Insert/delete at front Linked List O(1)
Search Hash Map O(1) avg
Insert/delete Balanced BST O(log n)
Insert / Extract min Min Heap O(log n)
Sort Array O(n log n)
BFS/DFS Graph (V+E) O(V+E)

Part 3: Behavioral Interviews

Senior behavioral interviews dig into leadership, judgment, and conflict — not just “tell me about a project.” The STAR format (Situation, Task, Action, Result) is the baseline, but senior interviews push harder on the “why.”

Categories to Prepare Stories For

Technical leadership:

  • [ ] A time you made a significant architectural decision — what was the tradeoff, who did you align with?
  • [ ] A time you pushed back on a technical direction — how did you frame it, what happened?
  • [ ] A time you simplified a system or removed complexity — why was it needed?

Handling failure:

  • [ ] A production incident you were responsible for — what happened, how did you respond, what changed after?
  • [ ] A project that failed or was cancelled — what did you learn, what would you do differently?
  • [ ] A technical debt situation you had to navigate

Collaboration and influence:

  • [ ] A time you disagreed with your manager or a senior stakeholder — how did you handle it?
  • [ ] A time you mentored someone — what did you focus on, how did it go?
  • [ ] A time you drove alignment across teams with conflicting priorities

Estimation and planning:

  • [ ] A time your estimate was wrong — what happened, how did you communicate it?
  • [ ] A time you had to scope down a project under deadline pressure — how did you decide what to cut?

Strong STAR Story Structure for Senior Roles

Weak answer: “I noticed a performance problem, I fixed it, the site got faster.”

Strong answer:

  • Situation: What was the business context? (Users were churning at a specific step, alerting was showing p99 latency over 3s)
  • Task: What was your specific responsibility? (I owned the checkout service, this was my problem to solve)
  • Action: What did you do — including the hard parts? (I profiled, found N+1 queries, proposed a solution, got pushback on timeline, negotiated a phased approach, implemented it over 2 sprints)
  • Result: Quantified outcome + what you’d do differently (p99 dropped from 3.1s to 340ms, checkout conversion +4%, in retrospect I should have added monitoring before the fix to baseline properly)

The “what I’d do differently” signals senior self-awareness.

Part 4: Code Review and Engineering Practices

Many senior interviews include a code review exercise — reading existing code and identifying issues. This tests your real-world judgment more than whiteboard algorithms.

What to Look For in Code Reviews

Correctness:

  • [ ] Off-by-one errors
  • [ ] Null/undefined handling
  • [ ] Edge cases (empty input, single element, maximum values)
  • [ ] Race conditions in concurrent code

Security:

  • [ ] SQL injection (string concatenation in queries)
  • [ ] XSS (unescaped user input rendered as HTML)
  • [ ] Hardcoded credentials
  • [ ] Missing input validation at API boundaries

Performance:

  • [ ] N+1 queries (fetching in a loop that could be one query)
  • [ ] Missing database indexes for query patterns
  • [ ] Unnecessary computation inside loops
  • [ ] Memory leaks (event listeners not cleaned up)

Maintainability:

  • [ ] Functions doing more than one thing
  • [ ] Magic numbers without named constants
  • [ ] Missing error handling
  • [ ] Test coverage for critical paths

Part 5: Domain-Specific Topics

Depending on the role, expect depth in one of these areas:

Frontend Senior

  • [ ] Browser rendering pipeline — critical rendering path, layout vs paint vs composite
  • [ ] Performance — Core Web Vitals (LCP, FID/INP, CLS), bundle analysis, code splitting
  • [ ] Accessibility — WCAG 2.1 AA, ARIA roles, keyboard navigation, screen reader testing
  • [ ] State management — when to use context vs external store, avoiding prop drilling
  • [ ] TypeScript — advanced types, generics, utility types, discriminated unions

Backend Senior

  • [ ] Database query optimization — EXPLAIN ANALYZE, index design, query rewriting
  • [ ] API design — idempotency, versioning, backward compatibility
  • [ ] Security — OWASP Top 10, authentication vs authorization, secrets management
  • [ ] Observability — structured logging, metrics, tracing, alert design
  • [ ] Containerization — Docker multi-stage builds, orchestration basics

Full-Stack Senior

  • [ ] Auth patterns — session vs JWT, OAuth 2.0 / OIDC flows, refresh token rotation
  • [ ] Deployment — CI/CD pipelines, blue-green deploys, feature flags, rollbacks
  • [ ] Monitoring — defining SLOs, alerting on symptoms vs causes

Interview Week Checklist

One week before:

  • [ ] Review 2-3 system design problems end-to-end
  • [ ] Practice coding one problem per day (medium difficulty, focus on communication)
  • [ ] Prepare 6-8 STAR stories covering the behavioral categories above
  • [ ] Research the company’s tech stack and any recent engineering blog posts

Day before:

  • [ ] Light review, no new material
  • [ ] Prepare your setup: quiet space, reliable internet, IDE ready
  • [ ] Have a glass of water and something to write on

During interviews:

  • [ ] Ask clarifying questions before you start
  • [ ] Narrate your thinking out loud
  • [ ] Name tradeoffs explicitly (“The downside of this approach is…”)
  • [ ] Ask for feedback at the end (“Is there a direction you’d have taken this instead?”)

The most honest advice: Senior interviews reward people who’ve thought deeply about their own work. The preparation that matters most isn’t grinding LeetCode — it’s being able to articulate why you made the choices you made in your career, what broke, what you’d do differently, and what you’re still figuring out.

Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.

Understanding REST API Design Best Practices

A REST API is a contract. Every decision you make when designing it — URL structure, status codes, error formats, versioning — becomes a constraint your consumers must work around. Get it right upfront, and integration is smooth. Get it wrong, and you’ll be maintaining backwards-compatibility hacks for years.

Here’s what separates a well-designed REST API from a frustrating one.

Think in Resources, Not Actions

REST is resource-oriented. URLs identify things, HTTP methods express what you’re doing to them.

# Wrong — action-based URLs
POST /createUser
GET  /getUserById?id=42
POST /deleteUser
POST /updateUserEmail

# Right — resource-based URLs
POST   /users          # create a user
GET    /users/42       # get a specific user
DELETE /users/42       # delete a user
PATCH  /users/42       # partially update a user

The HTTP method carries the semantic meaning. Your URL should only identify what resource is being acted on.

Nested Resources

Use nesting for resources that belong to a parent:

GET  /users/42/orders         # all orders for user 42
GET  /users/42/orders/7       # specific order
POST /users/42/orders         # create order for user 42

Limit nesting to one or two levels deep. Beyond that, use query parameters or flat resource paths.

HTTP Methods: Use Them Correctly

Each HTTP method carries a specific meaning that clients and infrastructure depend on.

Method Semantics Safe Idempotent
GET Read Yes Yes
POST Create No No
PUT Full replace No Yes
PATCH Partial update No No
DELETE Delete No Yes

Safe means the request doesn’t modify state — caches and intermediaries can call it freely.

Idempotent means calling it multiple times produces the same result — important for retries on network failure.

# PUT should fully replace the resource
PUT /users/42
{ "name": "Alice Smith", "email": "[email protected]", "role": "admin" }

# PATCH updates only the fields provided
PATCH /users/42
{ "email": "[email protected]" }

Status Codes: Say What You Mean

HTTP status codes are a semantic protocol. Using them correctly means clients can handle responses without parsing error messages.

Success (2xx)

200 OK          — general success (GET, PUT, PATCH)
201 Created     — resource was created (POST), include Location header
204 No Content  — success with no body (DELETE)

Client Errors (4xx)

400 Bad Request      — invalid input, validation failure
401 Unauthorized     — not authenticated (missing/invalid token)
403 Forbidden        — authenticated but not permitted
404 Not Found        — resource doesn't exist
409 Conflict         — state conflict (duplicate key, version mismatch)
422 Unprocessable    — valid format but semantic errors (can't process)
429 Too Many Requests — rate limited

The most common mistake: returning 200 OK with an error in the body. That makes every client parse the response body just to check if the request succeeded.

Consistent Error Responses

Define a standard error format and use it everywhere:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email address is invalid",
    "field": "email",
    "requestId": "req_abc123"
  }
}

Key principles:

  • Always include a machine-readable code (not just the HTTP status)
  • Include a human-readable message for debugging
  • Include a requestId so users can reference it in support tickets
  • For validation errors, include which field failed
// Multiple validation errors
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "requestId": "req_abc123",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "age", "message": "Must be 18 or older" }
    ]
  }
}

Versioning

APIs change over time. Versioning lets you make breaking changes without breaking existing integrations.

URL Versioning (Most Common)

GET /v1/users/42
GET /v2/users/42

Explicit, easy to understand, and works everywhere. The downside: technically violates REST (the version isn’t a resource), but this is the pragmatic choice for most teams.

Header Versioning

GET /users/42
Accept: application/vnd.myapi.v2+json

Cleaner URLs but harder to test in a browser. Use this if you have strong REST purists on the team or if your API is a long-lived platform product.

Rule: Once you publish a versioned API, never make breaking changes to it. Add fields (non-breaking), don’t remove them. Introduce new behavior in /v2, not by silently changing /v1.

Pagination

Never return unbounded lists. Always paginate collections.

Cursor-Based (Preferred)

GET /posts?cursor=abc123&limit=20

Response:
{
  "data": [...],
  "pagination": {
    "nextCursor": "def456",
    "hasMore": true
  }
}

Cursor pagination is stable — inserting new records doesn’t cause items to skip or duplicate across pages.

Offset-Based

GET /posts?page=3&per_page=20

Response:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "perPage": 20,
    "total": 147,
    "totalPages": 8
  }
}

Offset pagination is simpler to implement and supports random page access. It breaks under concurrent inserts, but that’s often acceptable for non-real-time data.

Filtering, Sorting, and Searching

Use query parameters for these operations:

GET /users?status=active                    # filter
GET /posts?sort=created_at&order=desc       # sort
GET /users?q=alice                          # search
GET /orders?created_after=2026-01-01        # date range
GET /products?category=tools&min_price=10  # combine filters

Keep query parameter names consistent across all your endpoints. If it’s sort on one endpoint, it shouldn’t be order_by on another.

Security Defaults

A few non-negotiable security practices:

Always use HTTPS. Redirect HTTP to HTTPS; don’t serve APIs over plain HTTP.

Return minimal data. Don’t include internal IDs, passwords, server paths, or internal state in responses. Think about what a consumer needs, not what’s convenient to serialize.

Set CORS correctly. Don’t use Access-Control-Allow-Origin: * for APIs that handle authenticated data.

Rate limit everything. Even internal APIs. Return 429 Too Many Requests with a Retry-After header when limits are hit:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711000000

Documentation as Contract

The best APIs treat documentation as part of the product, not an afterthought. Use OpenAPI/Swagger to define your API schema:

paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User found
        '404':
          description: User not found

An OpenAPI spec generates documentation, client SDKs, and mock servers. It’s the single source of truth for your API contract.

Summary

Good REST API design comes down to consistency and respecting the HTTP protocol:

  • URLs identify resources (nouns), methods express actions (verbs)
  • Use HTTP methods and status codes as intended
  • Define a standard error format and use it everywhere
  • Version from day one — breaking changes need new versions
  • Always paginate collections
  • Document with OpenAPI

The payoff is an API that developers can integrate confidently, that’s easy to maintain, and that grows without accumulating technical debt.

Level Up Your Dev Workflow

Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers.

🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included.