Next.js course Β· Module 9: Integrations and Advanced Features

after() API - Running code after the response

9 min read
In this lesson10

A user of Metropolis Quantum 2150 buys a ticket for an orbital flight and waits. The order was saved in 50 ms, but the server is still sending an email, updating the inventory and logging the event to the audit system, so the confirmation screen appears after two seconds. Cypher, the performance guardian of the Metropolis, knows the cure: the after() API - a function that lets you run code after the response has been sent to the user. It is the ideal solution for tasks such as logging, analytics or cleanup.

Problem: Blocking side operations

Imagine a typical scenario - a user visits a product page, and you want to:

  1. Return the product data (critical)
  2. Log the visit (secondary)
  3. Update statistics (secondary)
  4. Send an event to analytics (secondary)

Without after() every await lengthens the response time, even though the user only needs the product data. This is a Route Handler in which everything blocks the response:

1// Bad approach - everything blocks the response
2export async function GET(request: Request) {
3  const product = await getProduct(id);
4
5  // These operations delay the response!
6  await logVisit(id);
7  await updateStats(id);
8  await sendAnalytics('product_view', { id });
9
10  return Response.json(product);
11}

The user waits for the sum of all four operations, and three of them do not concern them at all.

Solution: the after() API

The after() function from next/server takes a callback and schedules it to run AFTER the response is finished. We move the side code into the callback:

1import { after } from 'next/server';
2
3export async function GET(request: Request) {
4  const product = await getProduct(id);
5
6  // Schedule execution after the response
7  after(async () => {
8    await logVisit(id);
9    await updateStats(id);
10    await sendAnalytics('product_view', { id });
11  });
12
13  // Response is sent immediately!
14  return Response.json(product);
15}

The response goes out right after the product is fetched, and logging and statistics run a moment later. The order of operations inside the callback did not change, only the moment they start.

Enabling the after() API

In Next.js 15.0 (from the RC to the stable release) the function was called unstable_after and required an experimental flag:

1// next.config.js - only Next.js 15.0 (unstable_after), unnecessary since 15.1
2module.exports = {
3  experimental: {
4    after: true,
5  },
6};

Since Next.js 15.1 after() is stable, so this flag is no longer needed and in Next.js 16 you simply import the function. It is also worth knowing that after() is not a request-time API: using it does not make a route dynamic, and on a static page the callback runs at build time or during revalidation.

Usage in different contexts

after() works in Server Components (including generateMetadata), Server Actions, Route Handlers and in Proxy (formerly Middleware).

In Route Handlers

A Route Handler is a route.ts file exporting a function named after an HTTP method, for example export async function POST(request: Request), which reads the body with await request.json(). This is how creating an order with side effects looks:

1// app/api/orders/route.ts
2import { after } from 'next/server';
3import { sendEmail, updateInventory, notifyWarehouse } from '@/lib/services';
4
5export async function POST(request: Request) {
6  const orderData = await request.json();
7
8  // Essential operation - create the order
9  const order = await createOrder(orderData);
10
11  // Side effects - run after the response
12  after(async () => {
13    // Confirmation email
14    await sendEmail({
15      to: orderData.email,
16      subject: 'Order confirmation',
17      template: 'order-confirmation',
18      data: order,
19    });
20
21    // Update inventory
22    await updateInventory(order.items);
23
24    // Notify the warehouse
25    await notifyWarehouse(order);
26
27    // Audit log
28    await auditLog('order_created', { orderId: order.id });
29  });
30
31  // Immediate response
32  return Response.json({
33    success: true,
34    orderId: order.id
35  });
36}

The client gets the orderId immediately, while the email, inventory and audit are handled in the background. In Route Handlers you can even call cookies() and headers() inside the after() callback.

In Server Components

In a server component after() tracks a page view after the page is rendered:

1// app/products/[id]/page.tsx
2import { after } from 'next/server';
3import { trackPageView } from '@/lib/analytics';
4
5export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
6  const product = await getProduct((await params).id);
7
8  // Tracking after the page renders
9  after(async () => {
10    await trackPageView({
11      page: 'product',
12      productId: (await params).id,
13      productName: product.name,
14      category: product.category,
15    });
16  });
17
18  return (
19    <div className="product-page">
20      <h1>{product.name}</h1>
21      <p>{product.description}</p>
22      <span className="price">${product.price}</span>
23    </div>
24  );
25}

Watch out for a limitation: in Server Components you must not call cookies() or headers() inside after(), because it throws an error. Read that data earlier, in the component body, and pass it to the callback through a closure.

In Server Actions

A Server Action first performs the critical operation and revalidates paths, then defers the rest:

1// app/actions/checkout.ts
2'use server';
3
4import { after } from 'next/server';
5import { revalidatePath } from 'next/cache';
6
7export async function completeCheckout(formData: FormData) {
8  const cartId = formData.get('cartId') as string;
9
10  // Essential operation
11  const order = await processPayment(cartId);
12
13  if (!order.success) {
14    return { error: 'Payment failed' };
15  }
16
17  // Revalidate paths
18  revalidatePath('/cart');
19  revalidatePath('/orders');
20
21  // Side effects after the response
22  after(async () => {
23    // Clear the cart in the background
24    await clearCart(cartId);
25
26    // Generate PDF invoice
27    await generateInvoice(order.id);
28
29    // External integrations
30    await syncWithERP(order);
31    await notifyShipping(order);
32
33    // Update the loyalty program
34    await updateLoyaltyPoints(order.userId, order.total);
35  });
36
37  return { success: true, orderId: order.id };
38}

The user sees the payment confirmation before the PDF invoice is generated. Revalidation stays before after(), because what the user sees depends on it.

Multiple after() calls

You can use after() many times - all callbacks will be executed:

1import { after } from 'next/server';
2
3export async function POST(request: Request) {
4  const data = await request.json();
5  const result = await processData(data);
6
7  // Logging
8  after(async () => {
9    await logger.info('Data processed', { id: result.id });
10  });
11
12  // Analytics
13  after(async () => {
14    await analytics.track('data_processed', {
15      processingTime: result.duration,
16      dataSize: data.length,
17    });
18  });
19
20  // Cleanup
21  after(async () => {
22    await cleanupTempFiles(result.tempPath);
23  });
24
25  return Response.json(result);
26}

Separate callbacks organize the code by responsibility: logging, analytics, cleanup. after() calls can also be nested, for example inside helper functions.

Error handling in after()

Errors in after() do not affect the response, because it has already reached the client, but they should be handled:

1import { after } from 'next/server';
2
3export async function GET(request: Request) {
4  const data = await fetchData();
5
6  after(async () => {
7    try {
8      await riskyOperation();
9    } catch (error) {
10      // Log the error but do not interrupt
11      console.error('After callback failed:', error);
12      await errorReporting.capture(error);
13    }
14  });
15
16  return Response.json(data);
17}

The try/catch block catches the error and sends it to the reporting system instead of letting it vanish. The callback runs even when the response ended with an error or with a call to notFound() or redirect().

Practical example - Notification system

Finally, a complete Route Handler for liking a post. Authorization and saving are critical, while notifications, statistics and ranking can wait:

1// app/api/posts/[id]/like/route.ts
2import { after } from 'next/server';
3import { getSession } from '@/lib/auth';
4
5export async function POST(
6  request: Request,
7  { params }: { params: Promise<{ id: string }> }
8) {
9  const session = await getSession();
10  if (!session) {
11    return Response.json({ error: 'Unauthorized' }, { status: 401 });
12  }
13
14  // Essential operation - add the like
15  const like = await addLike((await params).id, session.userId);
16
17  // All notifications and side effects after the response
18  after(async () => {
19    const post = await getPost((await params).id);
20
21    // Notify the post author
22    if (post.authorId !== session.userId) {
23      await createNotification({
24        userId: post.authorId,
25        type: 'like',
26        message: `${session.user.name} liked your post`,
27        link: `/posts/${(await params).id}`,
28      });
29
30      // Push notification
31      await sendPushNotification(post.authorId, {
32        title: 'New like!',
33        body: `${session.user.name} liked your post`,
34      });
35    }
36
37    // Update statistics
38    await incrementPostStats((await params).id, 'likes');
39
40    // Update the feed algorithm
41    await updateFeedRanking((await params).id, 'engagement');
42  });
43
44  return Response.json({ liked: true, likeCount: like.count });
45}

The session check stays before the response, because the 401 status depends on it. Everything that only informs others goes into after().

after() in special route files

The same model applies to special files in the app/ directory that Next.js treats as Route Handlers: app/sitemap.js, app/robots.js, app/manifest.js and app/opengraph-image.js. Each of them exports a default function, for example export default function sitemap():

1// app/sitemap.ts
2import type { MetadataRoute } from 'next';
3
4export default function sitemap(): MetadataRoute.Sitemap {
5  return [{ url: 'https://quantum.example.com/', lastModified: new Date() }];
6}

This file generates /sitemap.xml without writing XML by hand. A webhook from an external service follows a similar scheme: endpoint, signature verification, event processing, a 200 OK response. You can move heavy processing into after() to acknowledge receipt quickly.

When to use after()?

Ideal use cases:

  • Logging and auditing - recording user actions
  • Analytics - tracking events and metrics
  • Notifications - sending emails, push notifications
  • Cleanup - removing temporary files
  • Synchronization - updating external systems
  • Report generation - creating files in the background

Do not use it for:

  • Validation - must happen before the response
  • Authorization - must happen before the response
  • Data needed in the response - obviously!
  • Operations that need confirmation - the user will not see the result

Remember the time limit: after() runs at most as long as the route's default limit or the one set with maxDuration allows. On serverless platforms Next.js relies on the waitUntil mechanism, and static export does not support after() at all.

Summary

The after() API in Next.js is a powerful optimization tool:

  • Faster responses - side operations do not block the user
  • Better UX - the user sees the result immediately
  • Cleaner code - a clear separation of critical and side operations
  • Reliability - errors in after() do not break the response

My advice: at every await in a handler, ask whether the user really has to wait for it. If not, move it into after(). In the next lesson you will learn the use cache directive, which speeds up data fetching itself.

Remember: in Metropolis Quantum the response flies to the user at the speed of light, and after() quietly cleans up behind it.

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is the after() API in Next.js 16 used for?

  2. 2. Which directory in Next.js contains publicly accessible static assets?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Vertical ordering

    Arrange the elements in the correct order: app/ β†’ sitemap.js β†’ export default

  • Vertical ordering

    Arrange the elements in the correct order: Submit PR β†’ Review code β†’ Address feedback

  • Vertical ordering

    Arrange the elements in the correct order: app/ β†’ robots.js β†’ export default

  • Vertical ordering

    Arrange the elements in the correct order: Bump version β†’ Update changelog β†’ Tag release

  • Vertical ordering

    Arrange the elements in the correct order: app/ β†’ opengraph-image.js β†’ export default

  • Vertical ordering

    Arrange the steps of Stripe payment integration in Next.js

  • Vertical ordering

    Arrange the path and export of the manifest.js file

  • Vertical ordering

    Arrange the steps of handling a webhook from an external service

  • Click in order

    Arrange the syntax of an API route handler in Next.js App Router

  • Vertical ordering

    Arrange the elements in the correct order: Server Component fetch β†’ Client Component mount

  • Vertical ordering

    Arrange the elements in the correct order: export default β†’ function Page() β†’ {

  • Vertical ordering

    Arrange the steps of integrating a headless CMS with Next.js

Useful articles