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:
Feature | Status | Impact |
---|---|---|
Turbopack Builds | Beta | 2-5x faster build times |
Node.js Middleware | Stable | Full Node.js API access in middleware |
TypeScript Improvements | Stable | Better type safety for routes |
next lint deprecation | Warning phase | Migration to direct ESLint/Biome |
NextJS 16 prep | Deprecation warnings | Early 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:
- Customer site: 2x faster on 4-core machines
- Small sites (10K modules): 4x faster on 30-core machines
- Large sites (70K modules): 5x faster on 30-core machines
- Battle tested: These applications powered by Turbopack have been battle tested serving over 1.2 billion requests since the rollout.
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 Size | Cores | Recommendation | Expected Improvement |
---|---|---|---|
Small (<1K modules) | 4-8 cores | Webpack | Marginal improvement |
Medium (1K-10K modules) | 8+ cores | Try Turbopack | 1.5-2x faster |
Large (10K+ modules) | 12+ cores | Definitely Turbopack | 2-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:
- Remove all
export const config = { amp: true }
declarations - Remove
useAmp
imports and usage - Remove AMP configuration from
next.config.js
- Use NextJS’s built-in performance optimizations instead
Link legacyBehavior
// ❌ 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:
- Turbopack’s production readiness (2-5x build speed improvements)
- Node.js middleware stability (full backend capabilities in middleware)
- TypeScript enhancements (automatic route typing and better DX)
- Modern tooling migration (ESLint/Biome integration)
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
- Upgrade immediately if you’re starting a new project
- 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)
- 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.