Webhooks are a powerful way to enable real-time communication between applications. Instead of repeatedly polling for updates, webhooks notify a system when an event occurs. In this blog, weโll explore the concept of webhooks through a practical example using Next.js and NestJS. Weโll demonstrate how to build a webhook-driven system to update blog posts efficiently and leverage Static Site Generation (SSG) to optimize performance.
What Are Webhooks?
Webhooks are essentially HTTP callbacks triggered by events in an application. For instance, if a blog post is updated in the backend, the backend can fire a webhook to notify the frontend to refresh its cache or revalidate its static pages.
Why Use Webhooks?
- Efficiency: Reduces the need for frequent polling.
- Performance Optimization: Keeps cached data fresh without regenerating it unnecessarily.
- Scalability: Offloads repetitive tasks and minimizes resource usage.
- Real-time Updates: Ensures end-users always see the latest content.
Example: Using Webhooks in Next.js and NestJS
Letโs dive into an example where we use:
- Next.js to serve static content with SSG.
- NestJS as the backend to update blog posts.
When a blog post is updated in the backend, a webhook will notify the Next.js frontend to revalidate the affected page.
Setting Up the Frontend Webhook Endpoint in Next.js
Create an API route to handle incoming webhook events.
File: src/app/api/webhooks/blog/route.ts
typescriptimport { NextRequest, NextResponse } from "next/server"
import { revalidatePath } from "next/cache"
export const PATCH = async (request: NextRequest) => {
try {
const body = await request.json()
if (!body?.slug) {
return NextResponse.json({ message: "Invalid payload" }, { status: 400 })
}
// Revalidate the path for the updated blog post
const path = `/blog/${body.slug}`
await revalidatePath(path)
return NextResponse.json({ message: `Revalidated path: ${path}` }, { status: 200 })
} catch (error) {
console.error("Error revalidating path:", error)
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 })
}
}
This API route listens for PATCH
requests and uses the revalidatePath
function to refresh the static content for a specific blog post.
Implementing the Backend in NestJS
NestJS will handle blog post updates and trigger a webhook to notify the Next.js frontend.
File: src/blog/blog.service.ts
typescriptimport { Injectable, HttpService } from "@nestjs/common"
@Injectable()
export class BlogService {
constructor(private readonly httpService: HttpService) {}
async updateBlogPost(slug: string, content: string): Promise<void> {
// Your blog post updating logic
// Fire a webhook to notify Next.js
await this.triggerWebhook(slug)
}
private async triggerWebhook(slug: string): Promise<void> {
// Your frontend revalidation api URL
const webhookUrl = "http://localhost:3000/api/webhooks/blog"
try {
await this.httpService.patch(webhookUrl, { slug }).toPromise()
console.log("Webhook triggered successfully")
} catch (error) {
console.error("Error triggering webhook:", error)
}
}
}
This service updates a blog post and triggers a webhook to revalidate the corresponding static page.
File: src/blog/blog.controller.ts
typescriptimport { Controller, Patch, Body } from "@nestjs/common"
import { BlogService } from "./blog.service"
@Controller('blog')
export class BlogController {
constructor(private readonly blogService: BlogService) {}
@Patch()
async updateBlog(@Body("slug") slug: string, @Body("content") content: string) {
await this.blogService.updateBlogPost(slug, content)
return { message: "Blog post updated and webhook triggered" }
}
}
Securing Your Webhooks
To protect your webhook endpoint from unauthorized access and potential abuse, you can implement the following security measures:
- Secret Keys: Use a shared secret key to verify the authenticity of incoming webhook requests.
- Custom User-Agent Headers: Check for specific user-agent headers to ensure requests are coming from trusted sources.
Hereโs how to integrate these measures into your Next.js API:
File: src/app/api/webhooks/blog/route.ts
typescriptimport { NextRequest, NextResponse } from "next/server"
import { revalidatePath } from "next/cache"
const SECRET_KEY = process.env.WEBHOOK_SECRET_KEY!
const TRUSTED_USER_AGENT = "MyCustomUserAgent"
export const PATCH = async (request: NextRequest) => {
try {
const body = await request.json()
const receivedSecret = request.headers.get("x-webhook-secret")
const userAgent = request.headers.get("user-agent")
if (receivedSecret !== SECRET_KEY || userAgent !== TRUSTED_USER_AGENT) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 })
}
if (!body?.slug) {
return NextResponse.json({ message: "Invalid payload" }, { status: 400 })
}
// Revalidate the path for the updated blog post
const path = `/blog/${body.slug}`
await revalidatePath(path)
return NextResponse.json({ message: `Revalidated path: ${path}` }, { status: 200 })
} catch (error) {
console.error("Error revalidating path:", error)
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 })
}
}
By requiring a secret key and custom user-agent header, you add an extra layer of security to your webhook endpoint.
Advantages of This Approach
- Efficiency: Instead of regenerating all pages, only the affected page is revalidated.
- Improved Performance: Static pages remain cached until explicitly refreshed.
- Cost Savings: Reduces server load by avoiding unnecessary computations.
- Scalability: Supports large-scale applications with minimal effort.
Conclusion
Webhooks are an elegant solution for syncing data between systems in real time. By combining webhooks with Next.jsโs SSG capabilities, you can create highly efficient and performant applications. The example here highlights how to reduce server workload, improve user experience, and maintain up-to-date content seamlessly.