Plaintext Engineering

NextJS 15.5: The Game-Changer Release - Complete Guide with Examples

Aug 27, 2025 • 12 min read


NextJS 15.5 has just dropped, and honestly, this feels like one of the most significant releases in recent memory. Next.js 15.5 includes Turbopack builds in beta, stable Node.js middleware, TypeScript improvements, next lint deprecation, and deprecation warnings for Next.js 16. Let’s dive deep into what’s new and how you can start using these features today.

What’s New in NextJS 15.5: The Major Updates

Before we get into the technical details, here’s what caught my attention immediately:

FeatureStatusImpact
Turbopack BuildsBeta2-5x faster build times
Node.js MiddlewareStableFull Node.js API access in middleware
TypeScript ImprovementsStableBetter type safety for routes
next lint deprecationWarning phaseMigration to direct ESLint/Biome
NextJS 16 prepDeprecation warningsEarly migration guidance

Getting Started: Upgrading to NextJS 15.5

First things first - let’s get you upgraded:

# Use the automated upgrade CLI (recommended)
npx @next/codemod@canary upgrade latest

# Or upgrade manually
npm install next@latest react@latest react-dom@latest

# Or start a new project
npx create-next-app@latest my-app --typescript --tailwind --eslint --app

1. Turbopack Builds (Beta) - The Speed Revolution

This is the headline feature that everyone’s talking about. Turbopack now powers Vercel websites including vercel.com, v0.app, and nextjs.org, accelerating iteration velocity through faster preview and production deployment builds.

Performance Numbers That Matter

The performance improvements are genuinely impressive:

How to Enable Turbopack Builds

# Enable Turbopack for production builds
next build --turbopack

# Or configure it in your package.json
{
  "scripts": {
    "build": "next build --turbopack",
    "build:webpack": "next build", // Keep webpack as fallback
    "dev": "next dev --turbo" // You probably already have this
  }
}

When to Use Turbopack vs Webpack

Based on the official guidance and my testing:

Project SizeCoresRecommendationExpected Improvement
Small (<1K modules)4-8 coresWebpackMarginal improvement
Medium (1K-10K modules)8+ coresTry Turbopack1.5-2x faster
Large (10K+ modules)12+ coresDefinitely Turbopack2-5x faster

Production-Ready Example

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable experimental features for Turbopack
  experimental: {
    turbo: {
      // Configure Turbopack-specific options
      rules: {
        '*.svg': {
          loaders: ['@svgr/webpack'],
          as: '*.js',
        },
      },
    },
  },

  // Your existing config
  images: {
    domains: ['example.com'],
  },

  // Turbopack works with most existing configurations
  webpack: (config, { isServer }) => {
    // This will be ignored when using --turbopack
    // but kept for webpack builds
    return config;
  },
};

module.exports = nextConfig;

Known Limitations and Workarounds

Be aware of these current limitations:

// CSS ordering might differ - use CSS Modules for predictable styling
// styles/Button.module.css
.button {
  @layer components {
    padding: 1rem;
    border-radius: 0.5rem;
  }
}

// components/Button.tsx
import styles from './Button.module.css';

export function Button({ children }: { children: React.ReactNode }) {
  return (
    <button className={styles.button}>
      {children}
    </button>
  );
}

2. Node.js Middleware (Stable) - Full Power Unlocked

This is huge for complex applications. Previously, middleware was limited to the Edge Runtime, but now you have full Node.js capabilities.

Before vs After Comparison

// ❌ Before: Limited to Edge Runtime APIs
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  // Limited APIs only - no fs, no complex libraries
  const token = request.headers.get('authorization');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}
// ✅ Now: Full Node.js power in middleware
import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs';
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { Redis } from 'ioredis';

export const config = {
  runtime: 'nodejs', // Now stable!
};

const redis = new Redis(process.env.REDIS_URL);

export async function middleware(request: NextRequest) {
  // Full Node.js APIs available
  const token = request.headers.get('authorization')?.replace('Bearer ', '');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    // Use any Node.js library
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

    // Access file system
    const configPath = path.join(process.cwd(), 'config', 'permissions.json');
    const permissions = JSON.parse(fs.readFileSync(configPath, 'utf8'));

    // Use Redis for caching
    const cachedUser = await redis.get(`user:${decoded.userId}`);

    if (!cachedUser) {
      // Database operations, external API calls, etc.
      const user = await fetchUserFromDatabase(decoded.userId);
      await redis.setex(`user:${decoded.userId}`, 3600, JSON.stringify(user));
    }

    // Complex business logic
    if (!hasPermission(decoded.userId, permissions, request.url)) {
      return NextResponse.redirect(new URL('/unauthorized', request.url));
    }

    // Add custom headers
    const response = NextResponse.next();
    response.headers.set('x-user-id', decoded.userId);

    return response;
  } catch (error) {
    console.error('Auth middleware error:', error);
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

export const config = {
  matcher: ['/api/protected/:path*', '/dashboard/:path*', '/admin/:path*'],
  runtime: 'nodejs',
};

Real-World Use Cases for Node.js Middleware

// Advanced authentication with database integration
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcrypt';

export const config = {
  runtime: 'nodejs',
  matcher: '/api/auth/:path*',
};

const prisma = new PrismaClient();

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === '/api/auth/session') {
    const sessionToken = request.cookies.get('session-token')?.value;

    if (sessionToken) {
      // Direct database access in middleware
      const session = await prisma.session.findUnique({
        where: { token: sessionToken },
        include: { user: true },
      });

      if (session && session.expiresAt > new Date()) {
        // Add user info to headers for API routes
        const response = NextResponse.next();
        response.headers.set('x-user-data', JSON.stringify(session.user));
        return response;
      }
    }

    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  return NextResponse.next();
}

3. TypeScript Improvements - Type Safety Revolution

The TypeScript improvements in 15.5 are substantial. Let’s break down each enhancement:

Typed Routes (Now Stable)

// next.config.ts
const nextConfig = {
  typedRoutes: true, // Now stable!
};

export default nextConfig;

Now your routes are fully type-safe:

// File structure:
// app/
//   blog/
//     [slug]/
//       page.tsx
//   dashboard/
//     settings/
//       page.tsx

import Link from 'next/link';

function Navigation() {
  return (
    <nav>
      {/* ✅ Full type safety - TypeScript knows these routes exist */}
      <Link href="/blog/my-post?utm_source=nav">Blog Post</Link>
      <Link href="/dashboard/settings">Settings</Link>

      {/* ❌ TypeScript error - route doesn't exist */}
      <Link href="/nonexistent-route">Broken</Link>

      {/* ✅ Dynamic routes with type safety */}
      <Link href={`/blog/${postSlug}`}>Dynamic Post</Link>
    </nav>
  );
}

Route Props Helpers - No More Manual Typing

This is where things get really exciting:

// ❌ Before: Manual typing hell
interface PageProps {
  params: Promise<{ slug: string; category: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function BlogPost(props: PageProps) {
  const params = await props.params;
  const searchParams = await props.searchParams;

  return <div>Post: {params.slug}</div>;
}
// ✅ After: Automatic typing with full route context
// No imports needed - PageProps is globally available!

export default async function BlogPost(props: PageProps<'/blog/[category]/[slug]'>) {
  // TypeScript knows the exact shape of params and searchParams
  const params = await props.params; // { slug: string; category: string }
  const searchParams = await props.searchParams;

  return (
    <div>
      <h1>Category: {params.category}</h1>
      <h2>Post: {params.slug}</h2>
      {searchParams.theme && <div>Theme: {searchParams.theme}</div>}
    </div>
  );
}

// Works for layouts too!
export default function CategoryLayout(props: LayoutProps<'/blog/[category]'>) {
  return (
    <div>
      <h1>Category: {props.params.category}</h1>
      {props.children}
      {props.sidebar} {/* Parallel routes are fully typed too! */}
    </div>
  );
}

Parallel Routes Support

The new system handles parallel routes beautifully:

// File structure:
// app/dashboard/
//   @analytics/
//     page.tsx
//   @team/
//     page.tsx
//   layout.tsx
//   page.tsx

// layout.tsx - Fully typed parallel routes
export default function DashboardLayout(props: LayoutProps<'/dashboard'>) {
  return (
    <div className="dashboard">
      <main>{props.children}</main>
      <aside className="analytics-panel">
        {props.analytics} {/* TypeScript knows this exists! */}
      </aside>
      <div className="team-widget">
        {props.team} {/* And this too! */}
      </div>
    </div>
  );
}

Manual Type Generation

# New command for external type validation
next typegen

# Use in CI/CD pipelines
next typegen && tsc --noEmit

# Generate types for specific directory
next typegen ./src

This is particularly useful for:

// package.json
{
  "scripts": {
    "type-check": "next typegen && tsc --noEmit",
    "lint": "next typegen && eslint .",
    "test": "next typegen && jest",
    "ci": "next typegen && npm run type-check && npm run lint && npm run test"
  }
}

4. Linting Evolution - From next lint to Modern Tooling

Starting with Next.js 15.5, the next lint command shows a deprecation warning and will be removed in Next.js 16.

Migration Path: ESLint

# Use the codemod to migrate automatically
npx @next/codemod@latest next-lint-to-eslint-cli .

This creates a modern ESLint setup:

// eslint.config.mjs (generated)
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')];

export default eslintConfig;
// package.json (updated scripts)
{
  "scripts": {
    "lint": "eslint",
    "lint:fix": "eslint --fix"
  }
}

New Option: Biome Integration

When creating new projects, you can now choose Biome:

npx create-next-app@latest my-app
# Choose "Biome" when prompted for linter

This generates:

// biome.json
{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "nursery": {
        "useSortedClasses": "error"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  },
  "javascript": {
    "formatter": {
      "semicolons": "asNeeded",
      "trailingCommas": "es5"
    }
  }
}
// package.json for Biome projects
{
  "scripts": {
    "lint": "biome check",
    "lint:fix": "biome check --write",
    "format": "biome format --write"
  }
}

5. NextJS 16 Preparation - What’s Being Deprecated

AMP Support Removal

// ❌ Will be removed in NextJS 16
import { useAmp } from 'next/amp';

export const config = { amp: true };

export default function MyAmpPage() {
  const isAmp = useAmp();
  return <div>AMP enabled: {isAmp}</div>;
}

Migration strategy:

// ❌ Legacy approach (deprecated)
<Link href="/about" legacyBehavior>
  <a className="nav-link">About</a>
</Link>

// ✅ Modern approach
<Link href="/about" className="nav-link">
  About
</Link>

Image Quality and Local Patterns

// ❌ Will require explicit configuration in NextJS 16
<Image src="/photo.jpg" quality={100} alt="Photo" />
<Image src="/logo.svg?v=1.2" alt="Logo" />

// ✅ Configure explicitly for NextJS 16
// next.config.js
const nextConfig = {
  images: {
    qualities: [75, 100], // Allow quality={100}
    localPatterns: [
      {
        pathname: '/logo.svg',
        search: '**', // Allow any query parameters
      },
    ],
  },
};

Migration Checklist and Best Practices

Step-by-Step Migration Guide

# 1. Upgrade NextJS
npx @next/codemod@canary upgrade latest

# 2. Migrate linting
npx @next/codemod@latest next-lint-to-eslint-cli .

# 3. Enable TypeScript improvements
# Update next.config.ts:
// next.config.ts
const nextConfig = {
  typedRoutes: true, // Enable typed routes
  experimental: {
    turbo: {}, // Prepare for Turbopack
  },
};

export default nextConfig;
# 4. Test Turbopack builds
npm run build -- --turbopack

# 5. Update middleware if needed
# Add runtime: 'nodejs' to middleware config if you need Node.js APIs

Performance Testing

// Performance monitoring setup
// lib/performance.ts
export function measureBuildTime() {
  const start = Date.now();

  return {
    end: () => {
      const duration = Date.now() - start;
      console.log(`Build completed in ${duration}ms`);
      return duration;
    },
  };
}

// Use in your CI/CD
// scripts/build-benchmark.js
const { measureBuildTime } = require('../lib/performance');

async function benchmarkBuild() {
  console.log('Testing Webpack build...');
  const webpackTimer = measureBuildTime();
  await exec('npm run build:webpack');
  const webpackTime = webpackTimer.end();

  console.log('Testing Turbopack build...');
  const turboTimer = measureBuildTime();
  await exec('npm run build:turbo');
  const turboTime = turboTimer.end();

  console.log(`Turbopack is ${(webpackTime / turboTime).toFixed(2)}x faster`);
}

Real-World Implementation Examples

Enterprise Authentication Middleware

// middleware.ts - Production-ready auth
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { createHash } from 'crypto';
import { Redis } from '@upstash/redis';

export const config = {
  runtime: 'nodejs',
  matcher: ['/api/protected/:path*', '/dashboard/:path*'],
};

const redis = Redis.fromEnv();

interface UserSession {
  userId: string;
  role: string;
  permissions: string[];
  expiresAt: number;
}

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return redirectToLogin(request);
  }

  try {
    // Create cache key from token
    const tokenHash = createHash('sha256').update(token).digest('hex');
    const cacheKey = `session:${tokenHash}`;

    // Try Redis cache first
    const cachedSession = await redis.get<UserSession>(cacheKey);

    if (cachedSession && cachedSession.expiresAt > Date.now()) {
      return addUserHeaders(NextResponse.next(), cachedSession);
    }

    // Verify JWT if not in cache
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;

    // Fetch fresh user data
    const userSession: UserSession = {
      userId: decoded.userId,
      role: decoded.role,
      permissions: await fetchUserPermissions(decoded.userId),
      expiresAt: decoded.exp * 1000,
    };

    // Cache for future requests
    await redis.setex(cacheKey, 3600, JSON.stringify(userSession));

    return addUserHeaders(NextResponse.next(), userSession);
  } catch (error) {
    console.error('Auth middleware error:', error);
    return redirectToLogin(request);
  }
}

function redirectToLogin(request: NextRequest) {
  const loginUrl = new URL('/login', request.url);
  loginUrl.searchParams.set('from', request.nextUrl.pathname);
  return NextResponse.redirect(loginUrl);
}

function addUserHeaders(response: NextResponse, session: UserSession) {
  response.headers.set('x-user-id', session.userId);
  response.headers.set('x-user-role', session.role);
  response.headers.set('x-user-permissions', JSON.stringify(session.permissions));
  return response;
}

async function fetchUserPermissions(userId: string): Promise<string[]> {
  // Your database logic here
  return ['read', 'write'];
}

Complete TypeScript Setup

// app/blog/[category]/[slug]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';

// Fully typed with automatic route detection
export default async function BlogPost(props: PageProps<'/blog/[category]/[slug]'>) {
  const { category, slug } = await props.params;
  const searchParams = await props.searchParams;

  const post = await getBlogPost(category, slug);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>Category: {category}</p>
      {searchParams.preview && <div>Preview Mode</div>}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

// Typed metadata generation
export async function generateMetadata(
  props: PageProps<'/blog/[category]/[slug]'>
): Promise<Metadata> {
  const { category, slug } = await props.params;
  const post = await getBlogPost(category, slug);

  if (!post) {
    return {
      title: 'Post Not Found',
    };
  }

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url: `/blog/${category}/${slug}`,
    },
  };
}

// Type-safe static params generation
export async function generateStaticParams(): Promise<
  Array<{ category: string; slug: string }>
> {
  const posts = await getAllBlogPosts();

  return posts.map((post) => ({
    category: post.category,
    slug: post.slug,
  }));
}

Conclusion: Why NextJS 15.5 is a Game-Changer

NextJS 15.5 represents a significant maturation of the framework. The combination of:

Makes this release feel like a major leap forward rather than an incremental update.

The performance improvements alone justify the upgrade, but the developer experience enhancements make it a no-brainer for most projects.

Next Steps

  1. Upgrade immediately if you’re starting a new project
  2. Plan migration for existing projects, especially if you:
    • Have slow build times (try Turbopack)
    • Need complex authentication (Node.js middleware)
    • Want better type safety (TypeScript improvements)
  3. Prepare for NextJS 16 by addressing deprecation warnings

NextJS continues to evolve rapidly while maintaining stability - this release demonstrates that the framework is hitting its stride as the default React meta-framework.

Sources

Related articles