The True Cost of a Failed Release (It’s Not Just the Rollback)

Cross-posted from the Unitix Flow Blog

A failed release doesn’t cost you 1 hour of rollback. It costs you trust.

I talked to a team of 8 engineers recently. They had a failed release every 3-4 sprints. Each one looked small: 30 minutes to roll back, a few hours to debug, re-test by the next day.

But when we added up the real costs, the picture changed completely.

The Real Numbers

Direct cost per failure: $4,000–$9,000

  • Rollback execution: 30-60 min × 2-3 engineers
  • Debugging the root cause: 2-4 hours × 1-2 senior devs
  • QA re-test of the entire release: 4-8 hours
  • Incident review meeting: 1 hour × full team
  • Communication overhead: Slack threads, status updates, customer comms

Feature delay: 3–5 business days per incident

The feature that was supposed to ship? It sits in limbo while the team deals with the fallout. Multiply this across 3-4 failures per year.

Deployment fear tax: incalculable

This is the sneaky one. After a bad release:

  • Friday deploys get banned
  • Thursday becomes “risky”
  • Deploy windows shrink to Tuesday mornings with full team on standby
  • VP approval required for routine deploys

The Death Spiral

Here’s the pattern that kills teams:

Fewer deploys → larger batches → more risk per deploy → more failures → even fewer deploys

Each failure adds a new sign-off step. After a year, shipping a one-line fix takes 3 days because it needs to go through the same 7-step approval process as a major feature.

The Root Causes

After analyzing dozens of post-mortems, the root causes are surprisingly consistent:

  1. Untested feature combinations — individual branches pass CI, the combination breaks in staging
  2. Missing environment config — works locally and in staging, fails in prod because of a missing env var
  3. Skipped QA — “we’ll test it in production” (narrator: they didn’t)
  4. Scope creep after QA sign-off — “just one more small change” after testing is complete
  5. No tested rollback plan — the rollback script exists but hasn’t been tested in 6 months

The Prevention Framework

The fix isn’t zero failures — it’s minimizing blast radius:

Staging branches for integration testing — merge features into a staging branch first. Find integration bugs before they reach production.

QA gates that block deploy without sign-off — binary pass/fail before the deploy button is even available. Not “someone should probably test this.”

Scope lock after testing — once QA starts, the release scope is frozen. New features go to the next release.

One-click rollback — if rollback requires SSH + manual migrations + config changes, it’s not a rollback plan. It’s a prayer.

Automated post-deploy verification — health checks, smoke tests, and metric monitoring that run automatically after every deploy.

The Math That Matters

If your team ships 20 releases per year and 3 fail:

  • Direct cost: $12,000–$27,000/year
  • Feature delay: 9-15 business days lost
  • Process overhead: ~1 approval step added per failure = 3 extra steps per year

After 2 years, you’ve added 6 unnecessary approval steps that slow down every release — including the ones that would have been fine.

The goal isn’t perfection. It’s a process where failures are small, detected early, and recovered quickly.

We built Unitix Flow to make this prevention framework the default — staging branches, QA gates, scope lock, and one-click operations built into the release process.

Bun Test Has a Free API That Runs Jest-Compatible Tests 10x Faster

Bun’s built-in test runner: zero config, Jest-compatible, 10x faster.

import { describe, it, expect, mock } from 'bun:test'

describe('math', () => {
  it('adds', () => expect(1 + 1).toBe(2))
})

const fn = mock(() => 42)
fn()
expect(fn).toHaveBeenCalled()
bun test  # that's it

Jest 8.5s vs Vitest 3.2s vs Bun 0.8s for 1000 tests.

Need to automate data collection or build custom scrapers? Check out my Apify actors for ready-made tools, or email [email protected] for custom solutions.

Grafana k6 Has a Free API That Load Tests Your APIs With JavaScript

k6 is a load testing tool that uses JavaScript for test scripts. Run locally, in CI/CD, or in the cloud. Write tests like you write code — not XML configs.

Quick Start

# Install
brew install k6           # macOS
sudo apt install k6       # Ubuntu

# Run a test
k6 run script.js

Basic Load Test

import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = {
  vus: 50,             // 50 virtual users
  duration: '30s',     // for 30 seconds
}

export default function () {
  const res = http.get('https://api.example.com/posts')

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  })

  sleep(1)
}

Ramping (Realistic Traffic Patterns)

export const options = {
  stages: [
    { duration: '2m', target: 100 },   // ramp up to 100 users
    { duration: '5m', target: 100 },   // stay at 100
    { duration: '2m', target: 200 },   // spike to 200
    { duration: '5m', target: 200 },   // stay at 200
    { duration: '2m', target: 0 },     // ramp down
  ],
}

Thresholds (Pass/Fail Criteria)

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500'],      // 95% of requests < 500ms
    http_req_failed: ['rate<0.01'],        // <1% error rate
    'http_req_duration{status:200}': ['p(99)<1000'],
  },
}

Use in CI/CD: k6 exits with non-zero if thresholds fail.

API Testing

import http from 'k6/http'
import { check } from 'k6'

export default function () {
  // POST with JSON
  const payload = JSON.stringify({ name: 'test', email: '[email protected]' })
  const params = { headers: { 'Content-Type': 'application/json' } }

  const res = http.post('https://api.example.com/users', payload, params)

  check(res, {
    'created': (r) => r.status === 201,
    'has id': (r) => JSON.parse(r.body).id !== undefined,
  })
}

k6 vs JMeter vs Artillery

Feature k6 JMeter Artillery
Language JavaScript XML/GUI YAML
Resource usage Low (Go) High (Java) Medium (Node)
CI/CD Native Possible Native
Cloud option k6 Cloud BlazeMeter Artillery.io
Learning curve Easy Steep Easy

The Bottom Line

k6 is the developer-friendly load testing tool. JavaScript scripts, low resource usage, CI/CD native, and thresholds that fail your pipeline when performance degrades.

Need to automate data collection or build custom scrapers? Check out my Apify actors for ready-made tools, or email [email protected] for custom solutions.

AI Tools That Actually Pay You Back: A Developer’s Guide to Monetizing AI

AI Tools That Actually Pay You Back: A Developer’s Guide to Monetizing AI

=====================================================================================

As a developer, you’re likely no stranger to the world of artificial intelligence (AI) and its many applications. From chatbots to predictive analytics, AI has become an integral part of modern software development. But did you know that there are AI tools that can actually pay you back? In this article, we’ll explore some of the most promising AI tools that can help you monetize your skills and earn a return on investment.

Introduction to AI Monetization

Before we dive into the tools themselves, let’s talk about the concept of AI monetization. AI monetization refers to the process of using AI to generate revenue, either directly or indirectly. This can be achieved through a variety of means, including:

  • Building and selling AI-powered products or services
  • Offering AI-related consulting or training services
  • Creating and licensing AI-powered intellectual property
  • Participating in AI-related affiliate programs or partnerships

Tool 1: Google Cloud AI Platform

The Google Cloud AI Platform is a powerful tool that allows developers to build, deploy, and manage AI models at scale. With the AI Platform, you can create custom machine learning models using popular frameworks like TensorFlow and scikit-learn, and then deploy them to a cloud-based infrastructure.

To get started with the AI Platform, you’ll need to create a Google Cloud account and install the Google Cloud SDK. From there, you can use the following code example to deploy a simple machine learning model:

import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from google.cloud import aiplatform

# Load the dataset
df = pd.read_csv('data.csv')

# Split the data into training and testing sets
train_df, test_df = df.split(test_size=0.2, random_state=42)

# Create a random forest classifier
clf = RandomForestClassifier(n_estimators=100)

# Train the model
clf.fit(train_df.drop('target', axis=1), train_df['target'])

# Deploy the model to the AI Platform
ai_platform = aiplatform.Model(
    display_name='My Model',
    description='A simple random forest classifier'
)
ai_platform.deploy(clf, 'gs://my-bucket/model.pkl')

With the AI Platform, you can monetize your AI models by offering them as a service to customers, or by using them to build and sell AI-powered products.

Tool 2: Amazon SageMaker

Amazon SageMaker is another popular AI tool that allows developers to build, train, and deploy machine learning models. With SageMaker, you can create custom models using popular frameworks like TensorFlow and PyTorch, and then deploy them to a cloud-based infrastructure.

To get started with SageMaker, you’ll need to create an AWS account and install the AWS SDK. From there, you can use the following code example to deploy a simple machine learning model:


python
import pandas as pd
import torch
import torch.nn as nn
from sagemaker.pytorch import PyTorch

# Load the dataset
df = pd.read_csv('data.csv')

# Split the data into training and testing sets
train_df, test_df = df.split(test_size=0.2, random_state=42)

# Create a simple neural network
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(784, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = torch.relu(self.fc1(x))
        x = self.fc2(x)
        return x

# Train the model
model = Net()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
for epoch in range

GitHub Copilot Workspace Has a Free AI That Plans and Implements Code Changes Across Files

GitHub Copilot Workspace goes beyond autocomplete. You describe a task in natural language, it creates a plan, shows you which files need changes, and implements them — across your entire repository.

How It Works

  1. Open an issue or describe a task
  2. Workspace analyzes your repo and creates a plan
  3. Review the plan: which files change, what changes
  4. Edit the plan if needed
  5. Click Implement — Workspace writes the code
  6. Review diffs, test, create PR

The Key Difference

Feature Copilot (autocomplete) Copilot Workspace
Scope Current line/function Entire repo
Input Code context Natural language task
Output Code suggestions Plan + implementation
Files Single file Multi-file changes
Review Inline Full diff view

Example: “Add dark mode to the settings page”

Workspace would:

  1. Identify settings.tsx, theme.ts, globals.css
  2. Plan: “Add theme toggle component, update CSS variables, persist preference”
  3. Show you the plan before writing code
  4. Implement across all 3 files
  5. Let you review each change

What It’s Best At

  • Bug fixes from issue descriptions
  • Feature implementations across multiple files
  • Refactoring (rename, extract, reorganize)
  • Adding tests for existing code
  • Documentation updates

Limitations

  • Still in technical preview
  • Works best with well-described issues
  • Complex architectural changes need human guidance
  • May miss edge cases

The Bottom Line

Copilot Workspace is the future of AI-assisted development: task-level, not line-level. You describe what needs to change, review the plan, and let AI implement it.

Need to automate data collection or build custom scrapers? Check out my Apify actors for ready-made tools, or email [email protected] for custom solutions.

Gas-Aware Trading: Execute Only When Gas Is Cheap

Your trading bot spotted a perfect arbitrage opportunity between Uniswap and Balancer. The price difference is 2.5% — enough for solid profit. But gas is sitting at 80 gwei. By the time the transaction confirms, the opportunity vanishes and you’re left holding the gas bill.

This scenario plays out thousands of times daily in DeFi. Trading bots either miss opportunities waiting for cheap gas or burn through profits on expensive transactions. What if your bot could automatically execute trades only when gas prices meet your profitability threshold?

Why Gas-Aware Execution Matters for Trading Bots

Gas costs can make or break trading strategies. A profitable arbitrage at 20 gwei becomes a loss at 100 gwei. MEV bots competing for the same opportunities often end up in gas wars, driving costs through the roof. Manual gas monitoring doesn’t scale when you’re running strategies across multiple chains and protocols.

The challenge isn’t just gas prices — it’s coordination. Your bot needs to:

  • Monitor gas prices across networks in real-time
  • Queue transactions with conditional execution
  • Cancel outdated opportunities before they become losses
  • Maintain execution across 14 DeFi protocols simultaneously
  • Handle cross-chain arbitrage where timing matters

Building this infrastructure from scratch means maintaining gas price feeds, transaction queuing systems, and integrations with dozens of protocols. That’s months of development before you even start on your actual trading logic.

Gas Conditional Execution: Your Bot Trades Smarter

WAIaaS solves this with gas conditional execution built into its 7-stage transaction pipeline. Your bot submits trades with gas price thresholds, and the system automatically executes only when conditions are met.

Here’s how it works in practice. Your arbitrage bot spots an opportunity and submits a conditional trade:

curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{
    "inputMint": "So11111111111111111111111111111111111111112",
    "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "1000000000",
    "gasCondition": {
      "maxGasPrice": "50000000",
      "timeout": 300
    }
  }'

The transaction enters the pipeline and waits at stage 4 (wait) until gas drops below 50 lamports. If gas stays high for 300 seconds, the transaction expires automatically. No manual cancellation, no wasted gas on unprofitable trades.

Multi-Protocol Trading Through One API

Trading bots need access to liquidity across protocols. WAIaaS integrates 14 DeFi protocols through a unified API, so your bot can execute complex strategies without managing separate SDKs.

Execute a cross-protocol arbitrage in three API calls:

# 1. Swap on Jupiter (Solana)
curl -X POST http://127.0.0.1:3100/v1/actions/jupiter-swap/swap 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"inputMint": "SOL", "outputMint": "USDC", "amount": "10000000000"}'

# 2. Bridge to Ethereum via LI.FI
curl -X POST http://127.0.0.1:3100/v1/actions/lifi/bridge 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"fromChain": "solana", "toChain": "ethereum", "token": "USDC", "amount": "1000"}'

# 3. Lend on Aave v3
curl -X POST http://127.0.0.1:3100/v1/actions/aave-v3/supply 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{"asset": "USDC", "amount": "1000"}'

Each action goes through the same gas-aware pipeline. Your bot submits the strategy, and WAIaaS handles execution timing across all three protocols.

Risk Controls That Scale

Trading bots need guardrails to prevent runaway losses. WAIaaS provides a 21-policy risk management system that operates at the wallet level, not per-strategy.

Set up automatic risk limits:

curl -X POST http://127.0.0.1:3100/v1/policies 
  -H "Content-Type: application/json" 
  -H "X-Master-Password: my-secret-password" 
  -d '{
    "walletId": "<wallet-uuid>",
    "type": "SPENDING_LIMIT",
    "rules": {
      "instant_max_usd": 1000,
      "notify_max_usd": 5000,
      "delay_max_usd": 20000,
      "delay_seconds": 300,
      "daily_limit_usd": 50000
    }
  }'

Trades under $1000 execute instantly. Trades between $1000-$5000 send notifications but still execute. Trades between $5000-$20000 wait 5 minutes (cancellable if your bot detects the opportunity expired). Trades over $20000 require manual approval.

You can also restrict protocols, tokens, and trading venues:

# Only allow trading on whitelisted protocols
curl -X POST http://127.0.0.1:3100/v1/policies 
  -H "X-Master-Password: my-secret-password" 
  -d '{
    "type": "CONTRACT_WHITELIST",
    "rules": {
      "contracts": [
        {"address": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", "name": "Jupiter"},
        {"address": "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9", "name": "Aave"}
      ]
    }
  }'

Simulation and Backtesting

Before risking capital, test strategies with the dry-run API. Submit any transaction with "dryRun": true to see exactly what would happen:

curl -X POST http://127.0.0.1:3100/v1/transactions/send 
  -H "Content-Type: application/json" 
  -H "Authorization: Bearer wai_sess_<token>" 
  -d '{
    "type": "TRANSFER",
    "to": "recipient-address",
    "amount": "0.1",
    "dryRun": true
  }'

The response shows gas estimates, policy decisions, and potential errors without executing the transaction. Essential for backtesting strategies against historical gas prices.

High-Performance Bot Integration

Trading bots need minimal latency overhead. WAIaaS provides both REST APIs and TypeScript/Python SDKs optimized for high-frequency strategies:

import { WAIaaSClient } from '@waiaas/sdk';

const client = new WAIaaSClient({
  baseUrl: 'http://127.0.0.1:3100',
  sessionToken: process.env.WAIAAS_SESSION_TOKEN,
});

// Check opportunity profitability
const balance = await client.getBalance();
const gasPrice = await client.getGasPrice();

if (gasPrice < profitabilityThreshold) {
  // Execute arbitrage
  const tx = await client.executeAction('jupiter-swap', {
    inputMint: 'SOL',
    outputMint: 'USDC',
    amount: balance.balance
  });
}

The SDK handles connection pooling, request retries, and error handling so your bot focuses on trading logic, not infrastructure.

Quick Start: Gas-Aware Trading Bot

Get started with gas conditional execution in 5 steps:

  1. Deploy WAIaaS daemon:
git clone https://github.com/minhoyoo-iotrust/WAIaaS.git
cd WAIaaS
docker compose up -d
  1. Create a trading wallet:
npm install -g @waiaas/cli
waiaas quickset --mode mainnet
  1. Set up risk policies (spending limits, protocol whitelist, gas thresholds)

  2. Install the SDK:

npm install @waiaas/sdk
  1. Submit your first gas-conditional trade using the Jupiter swap example above

Your bot now executes trades only when gas conditions are favorable, with automatic risk controls and multi-protocol access.

What’s Next

This is just the beginning. WAIaaS supports perpetual futures on Hyperliquid, prediction markets on Polymarket, liquid staking, cross-chain bridging, and more. Each protocol integration includes gas-aware execution and risk controls out of the box.

Ready to build smarter trading bots? Check out the full documentation and examples at https://github.com/minhoyoo-iotrust/WAIaaS or visit https://waiaas.ai to get started.

Reflective — AI journaling companion built with Notion MCP and Claude

This is a submission for the Notion MCP Challenge

What I Built

Reflective is a Chrome extension + Node.js backend that adds an AI journaling companion to your browser sidebar while you write in Notion.

Most journaling tools are write-only. You pour thoughts in, they sit there. Reflective makes your Notion journal a two-way conversation — without leaving the page.

How it works:

You write in Notion
       ↓
Click "Analyze this entry" in the sidebar
       ↓
Claude reads your entry + your journal history from Notion
       ↓
Opens a conversation grounded in what you actually wrote
       ↓
Click "Mark session complete"
       ↓
Mood score, tags, themes, and AI summary written back to Notion

Key behaviors:

  • Sits in the Chrome side panel alongside Notion — no new tabs
  • Context-aware: on a database view it loads your last 50 entries; on a single entry it reads just that page
  • Temporal awareness: if you’re reading an old entry, Claude knows — it frames responses as looking back, not as if it’s today
  • One conversation across the whole session — navigating to a new entry appends to the chat with a — now reading: [title] — divider
  • Analysis is always user-initiated. The extension makes zero API calls on navigation.
  • Mood sparkline shows your last 14 days, pulled live from a Notion database

One-click workspace setup. On first launch, it creates a Journal Entries database, Mood Log, Weekly Summaries database, a seeded starter entry, and a dashboard page — all via the Notion API. You paste your integration token, it does the rest in ~10 seconds.

Video Demo

Show Us the Code

Repo: https://github.com/neicore/notion-reflective

Stack:

  • Chrome Extension (Manifest V3, React + TypeScript, Vite, Tailwind)
  • Node.js + Express backend (TypeScript)
  • Anthropic Claude (claude-sonnet-4-5)
  • Notion API (@notionhq/client v2)
  • Server-Sent Events for streaming progress to the extension
  • pnpm workspaces

How I Used Notion MCP

Notion is where users read and write their data — and it’s also the entire data model. Every piece of state lives there, accessed via @notionhq/client.

Reading: building context for Claude

When you click “Analyze this entry”, the backend fetches in parallel:

const [rawHistory, recentMoods] = await Promise.all([
  fetchJournalEntries(journalDbId, 50, notionToken),  // last 50 entries
  queryMoodHistory(moodLogDbId, 14, notionToken),      // 14-day sparkline
])

Then reads the current page blocks directly:

const [pageObj, blocks] = await Promise.all([
  notion.pages.retrieve({ page_id: pageId }),
  notion.blocks.children.list({ block_id: pageId, page_size: 100 }),
])

The block content becomes the raw text fed to Claude alongside journal history. History is split into “before this date” and “after this date” relative to the entry you’re viewing — that’s what gives Claude correct temporal framing.

Writing: analysis flows back into Notion

When you mark a session complete:

await notion.pages.update({
  page_id: pageId,
  properties: {
    'Mood Score':       { number: result.moodScore },
    'Mood Tags':        { multi_select: result.moodTags.map(name => ({ name })) },
    'Themes':           { multi_select: result.themes.map(name => ({ name })) },
    'AI Summary':       { rich_text: [{ text: { content: result.aiSummary } }] },
    'Word Count':       { number: wordCount },
    'Session Complete': { checkbox: true },
  },
})

Your Notion database gets richer over time. On future analyses, entries with an AI Summary are used as-is (fast); entries without one get their blocks fetched (slower, shown in the loading progress bar). First load hydrates your journal — subsequent loads are instant.

The loading experience

Hydrating 50 entries takes a while the first time. The /api/init endpoint streams progress via Server-Sent Events:

data: {"type":"journal_count","total":47}
data: {"type":"hydrating","index":1,"total":47,"title":"March reflections"}
data: {"type":"hydrating","index":2,"total":47,"title":"A hard week"}
...
data: {"type":"done","entryContent":"...","openingMessage":"..."}

The extension consumes this with fetch + ReadableStream (more reliable than EventSource in extension contexts), updating the UI: Reading your journal — 3/47 — A hard week.

What Notion unlocks that a plain database wouldn’t

Notion’s multi-select properties for Mood Tags and Themes mean once entries are analyzed, you can filter, sort, and group your entire journal by mood or theme natively in Notion — no custom query interface needed. The AI populates the properties; Notion’s built-in views do the rest.

The mood sparkline pulls from a separate Mood Log database that tracks daily mood independently from journal entries. You can log mood on days you don’t write, and the chart reflects it. Two related databases — the kind of structure that’s natural in Notion and would need real schema design anywhere else.

That’s the part I keep coming back to: the AI populates the properties, Notion’s built-in views do the rest. No custom query interface, no separate dashboard to maintain. Your journal just gets smarter over time.

Fresh 2 Has a Free API That Brings Zero-JS Pages to Deno With Island Architecture

Fresh is Deno’s web framework. Zero JavaScript shipped to the client by default. Interactive parts are “islands” that hydrate independently. Fresh 2 adds Preact Signals, better plugins, and faster builds.

Quick Start

deno run -A https://fresh.deno.dev my-app
cd my-app && deno task start

Zero JS by Default

Every Fresh page is server-rendered HTML. No JavaScript bundle. This page component ships 0 bytes of JS:

// routes/index.tsx
export default function Home() {
  return (
    <div>
      <h1>Hello Fresh</h1>
      <p>This page has zero client-side JavaScript.</p>
    </div>
  )
}

Islands (Interactive Components)

Only components in /islands/ get hydrated on the client:

// islands/Counter.tsx
import { useSignal } from "@preact/signals"

export default function Counter() {
  const count = useSignal(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  )
}
// routes/index.tsx
import Counter from "../islands/Counter.tsx"

export default function Home() {
  return (
    <div>
      <h1>Static content (0 JS)</h1>
      <Counter />  {/* Only this hydrates */}
    </div>
  )
}

Data Loading with Handlers

// routes/posts/[id].tsx
import { Handlers, PageProps } from "$fresh/server.ts"

export const handler: Handlers = {
  async GET(_req, ctx) {
    const post = await fetchPost(ctx.params.id)
    if (!post) return ctx.renderNotFound()
    return ctx.render(post)
  }
}

export default function PostPage({ data }: PageProps) {
  return <article><h1>{data.title}</h1><p>{data.body}</p></article>
}

Fresh vs Next.js vs Remix

Feature Fresh Next.js Remix
Runtime Deno Node Node
Default JS 0 bytes Bundle Bundle
Hydration Islands Full/Partial Full
State Preact Signals React React

The Bottom Line

Fresh is perfect for content-heavy sites that need minimal interactivity. Zero JS default + island architecture = fastest possible page loads.

Need to automate data collection or build custom scrapers? Check out my Apify actors for ready-made tools, or email [email protected] for custom solutions.

Remix v2 Has a Free Framework That Makes React Server-Side Rendering Actually Enjoyable

Remix v2 ditches the file-based routing complexity of Next.js and gives you nested routes with built-in data loading. Every route is a server component by default. No client-side waterfalls. No loading spinners everywhere.

What Changed in v2

Feature Remix v1 Remix v2
Routing file convention v1 flat routes (v2 convention)
Dev server Custom Vite
CSS Custom imports Vite CSS
Meta Object Function
Error boundary CatchBoundary + ErrorBoundary Single ErrorBoundary

Quick Start

npx create-remix@latest my-app
cd my-app && npm run dev

Nested Routes with Data Loading

// app/routes/posts.$postId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"
import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await db.post.findUnique({ where: { id: params.postId } })
  if (!post) throw new Response("Not Found", { status: 404 })
  return json({ post })
}

export default function Post() {
  const { post } = useLoaderData<typeof loader>()
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>
}

Actions (Server-Side Form Handling)

import { ActionFunctionArgs, redirect } from "@remix-run/node"

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData()
  const title = formData.get("title") as string
  await db.post.create({ data: { title } })
  return redirect("/posts")
}

export default function NewPost() {
  return (
    <form method="post">
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  )
}

No useState. No onSubmit handler. No fetch calls. The form just works — with progressive enhancement.

Error Boundaries

export function ErrorBoundary() {
  const error = useRouteError()
  if (isRouteErrorResponse(error)) {
    return <div>{error.status}: {error.data}</div>
  }
  return <div>Something went wrong</div>
}

One boundary handles both expected (404) and unexpected errors per route.

Why Remix Over Next.js

  • Nested routes = parallel data loading (no waterfall)
  • Forms work without JavaScript
  • Standard Web APIs (Request, Response, FormData)
  • Simpler mental model than RSC

The Bottom Line

Remix v2 on Vite is fast, simple, and web-standards-first. If you want React SSR without the Next.js complexity, this is it.

Need to automate data collection or build custom scrapers? Check out my Apify actors for ready-made tools, or email [email protected] for custom solutions.