AI & Automations6 min read

Building an AI-Powered Content Pipeline with Next.js, Node, and Python

I faced the challenge of orchestrating LLM‑driven content generation, storage, and preview within a single Next.js 15 app. By combining a Python microservice, a Node.js API layer, and Sanity CMS, I created a reproducible, production‑ready workflow.

PK
Piyush Kalsariya
Aug 22, 2026·Full-Stack & AI Engineer
Building an AI-Powered Content Pipeline with Next.js, Node, and Python

Introduction

When I first experimented with large language models (LLMs) for blog post generation, I quickly realized that a naïve copy‑paste approach would not scale. The core problem was architecting a reliable end‑to‑end pipeline that could:

  • Accept a prompt from a Next.js UI.
  • Invoke an LLM (OpenAI, Anthropic, or locally hosted) via a Python service.
  • Store the generated markdown in Sanity CMS for editorial review.
  • Render a live preview in the same Next.js app.

In this post I walk through the architecture, the code that ties the pieces together, and the production lessons I learned.

---

High‑Level Architecture

``mermaid
1flowchart TD
2    subgraph Frontend[Next.js 15 (React/TS)]
3        UI[Prompt UI] --> API[API Route]
4    end
5    subgraph Backend[Node.js (Express)]
6        API --> PY[Python LLM Service]
7    end
8    subgraph Storage[Sanity CMS]
9        PY -->|store markdown| SANITY[Document]
10        SANITY -->|webhook| REVALIDATE[Next.js Revalidate]
11    end
12    UI -->|preview| REVALIDATE

Key components:

  • Next.js 15 – Handles the UI, static generation, and ISR revalidation.
  • Node.js API layer – Thin Express/Next.js API route that forwards prompts.
  • Python microservice – Runs the LLM inference (e.g., ``openai.ChatCompletion) and returns markdown.
  • Sanity CMS – Source of truth for content, with webhooks to trigger ISR.

---

Prompt UI in Next.js (TSX)

``tsx
1import { useState } from "react";
2import axios from "axios";
3
4export default function PromptForm() {
5  const [prompt, setPrompt] = useState("");
6  const [loading, setLoading] = useState(false);
7  const [slug, setSlug] = useState<string | null>(null);
8
9  const submit = async () => {
10    setLoading(true);
11    const res = await axios.post("/api/generate", { prompt });
12    setSlug(res.data.slug);
13    setLoading(false);
14  };
15
16  return (
17    <div className="max-w-xl mx-auto p-4">
18      <textarea
19        className="w-full h-32 p-2 border"
20        placeholder="Describe the article you want..."
21        value={prompt}
22        onChange={e => setPrompt(e.target.value)}
23      />
24      <button
25        className="mt-2 px-4 py-2 bg-blue-600 text-white"
26        onClick={submit}
27        disabled={loading || !prompt}
28      >
29        {loading ? "Generating…" : "Generate"}
30      </button>
31      {slug && (
32        <p className="mt-4">
33          🎉 Your draft is ready: <a href={```/posts/${slug}`} className="underline">View Preview</a>
34        </p>
35      )}
36    </div>
37  );
38}

The UI simply posts the prompt to ``/api/generate. The response contains a Sanity slug that we can use for preview.

---

Node.js API Route (TypeScript)

``typescript
1// pages/api/generate.ts
2import type { NextApiRequest, NextApiResponse } from "next";
3import axios from "axios";
4import { createClient } from "@sanity/client";
5
6const sanity = createClient({
7  projectId: process.env.SANITY_PROJECT_ID!,
8  dataset: "production",
9  token: process.env.SANITY_TOKEN!,
10  useCdn: false,
11});
12
13export default async function handler(req: NextApiRequest, res: NextApiResponse) {
14  if (req.method !== "POST") return res.status(405).end();
15  const { prompt } = req.body as { prompt: string };
16
17  // Forward to Python service
18  const llmRes = await axios.post(```${process.env.PY_SERVICE_URL}/generate`, { prompt });
19  const markdown = llmRes.data.content as string;
20
21  // Create a draft document in Sanity
22  const slug = `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
23  await sanity.create({
24    _type: "post",
25    title: "AI Draft – " + prompt.slice(0, 30) + "…",
26    slug: { _type: "slug", current: slug },
27    body: [{ _type: "block", children: [{ _type: "span", text: markdown }] }],
28    _createdAt: new Date().toISOString(),
29    draft: true,
30  });
31
32  // Return slug for preview
33  res.status(200).json({ slug });
34}

The route does three things:

  1. Calls the Python microservice.
  2. Persists the markdown as a Sanity draft.
  3. Returns the slug for the front‑end preview.

---

Python LLM Service (FastAPI)

````python
1# app/main.py
2import os
3from fastapi import FastAPI, HTTPException
4from pydantic import BaseModel
5import openai
6
7app = FastAPI()
8
9class Prompt(BaseModel):
10    prompt: str
11
12openai.api_key = os.getenv("OPENAI_API_KEY")
13
14@app.post("/generate")
15async def generate(prompt: Prompt):
16    try:
17        resp = openai.ChatCompletion.create(
18            model="gpt-4o-mini",
19            messages=[{"role": "user", "content": prompt.prompt}],
20            temperature=0.7,
21        )
22        content = resp.choices[0].message.content
23        return {"content": content}
24    except Exception as e:
25        raise HTTPException(status_code=500, detail=str(e))

I chose FastAPI for its async support and automatic OpenAPI docs. The service is containerised and runs behind an Nginx reverse proxy, exposing only ``/generate.

---

ISR Preview & Sanity Webhook

Next.js ISR (Incremental Static Regeneration) lets us serve a static preview that updates when the draft changes.

``typescript
1// pages/posts/[slug].tsx
2import { GetStaticProps, GetStaticPaths } from "next";
3import { createClient } from "@sanity/client";
4
5export const getStaticPaths: GetStaticPaths = async () => ({
6  paths: [],
7  fallback: "blocking",
8});
9
10export const getStaticProps: GetStaticProps = async ({ params }) => {
11  const slug = params?.slug as string;
12  const client = createClient({
13    projectId: process.env.SANITY_PROJECT_ID!,
14    dataset: "production",
15    useCdn: false,
16    token: process.env.SANITY_TOKEN!,
17  });
18  const post = await client.fetch(```*[_type == "post" && slug.current == $slug][0]`, { slug });
19  return { props: { post }, revalidate: 10 };
20};

A Sanity webhook (configured in the dashboard) calls ``/api/revalidate?slug=… which triggers res.revalidate('/posts/${slug}'). This guarantees the author sees the latest AI draft within seconds.

---

Pros & Cons

| Aspect | Pros | Cons |

|--------|------|------|

| Language separation | Python excels at LLM calls; Node handles web‑scale APIs. | Two runtimes increase DevOps surface area. |

| Sanity as source of truth | Real‑time editing, versioning, and preview hooks. | Vendor lock‑in; cost grows with document count. |

| ISR | Near‑instant preview without full SSR load. | Stale content if webhook fails; need fallback revalidation. |

| Containerisation | Each service can be scaled independently. | Requires orchestration (Docker Compose or k8s). |

---

Production Takeaways

  1. Keep the LLM call isolated – A Python microservice lets you swap models, add caching (Redis), or move to a GPU node without touching the Next.js code.
  2. Leverage Sanity drafts – Draft mode prevents accidental publishing and gives editors a familiar UI.
  3. Secure the pipeline – API routes must validate the prompt length, and the Python endpoint should require an API key header.
  4. Cache responses – For identical prompts, store the markdown in Redis for 5‑10 minutes to avoid rate‑limit throttling.
  5. Monitor webhook health – Implement a retry queue (e.g., BullMQ) for failed ISR revalidations.

---

Conclusion

By stitching together Next.js 15, a Node.js façade, a Python LLM worker, and Sanity CMS, I turned a chaotic “copy‑paste LLM” experiment into a production‑grade content engine. The pattern scales: you can add image generation, SEO meta‑tags, or multi‑language support with minimal friction. If you’re building AI‑augmented authoring tools, start with this modular architecture and iterate on caching, observability, and model selection.

---

Happy coding!

Tags:#Next.js#LLM#Sanity CMS#Python
PK

Written by Piyush Kalsariya

Full-stack software engineer and AI automation builder specializing in Next.js, Node.js, Python, Sanity CMS, and production LLM orchestration.