Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
149045 stories
·
33 followers

How to Build a NestJS AI Chatbot with Google Gemini

1 Share

In this post, we will build a real-time FAQ chatbot for a tax management SaaS using Gemini 2.5 Flash and NestJS with Server-Sent Events (SSE) to stream replies to the user. You'll learn how to set up Gemini and understand how context caching works.

In this post, we’ll build a real-time FAQ chatbot for a tax management SaaS using Gemini 2.5 Flash and NestJS with Server-Sent Events (SSE) to stream replies to the user. You’ll learn how to set up Gemini, understand how context caching in Gemini works while using implicit caching in our project, and how to work with SSE, async generators and observables in NestJS.

Prerequisites

To follow along and get the most from this post, you’ll need to have:

  • Basic familiarity with cURL
  • Google Gemini API key from AI Studio
  • Basic understanding of NestJS and TypeScript

What Is Google Gemini?

It’s Google’s multimodal AI model, capable of handling text, images, audio, video, code and embeddings. It’s used in chatbots, summarizations, embeddings for RAG (Retrieval Augmented Generation), code analysis and other forms of reasoning and generation.

Gemini has diverse models that excel at different tasks, the most prominent being:

  • 2.5 Pro: Best for complex reasoning, supports explicit caching
  • 2.5 Flash: More balanced with less reasoning capacity, but improved speed and lower cost
  • 2.5 Flash-8B: Best for high-frequency tasks and is the cheapest

For our FAQ chatbot, we’ll use 2.5 Flash because of its speed. It is well-suited for streaming, and its balanced reasoning makes it great for answering user queries.

Understanding Context Caching in Gemini

Gemini models now feature large context windows of up to 1 million tokens. This allows us to supply Gemini with extensive data for a task, even if only a small portion is needed. It can independently identify and use relevant data.

Context caching in Gemini helps it remember repeated input, which enables it to save input tokens and sometimes reduce latency. This is helpful when working with large prompts that first establish context for Gemini to use in generating responses, such as data for answering FAQ queries.

There are two types of context caching: implicit and explicit caching.

Implicit Caching

This feature is automatically enabled for Gemini 2.5 models. Gemini automatically caches repeated input, and when a request hits the cache, cost savings are automatically passed on without any configuration from us. Although implicit caches aren’t guaranteed, they are available on the Gemini free tier and don’t require any additional setup.

A basic requirement for context caching is that the input token count be at least 1,024 for 2.5 Flash and 4,096 for 2.5 Pro. To increase the chances of an implicit cache hit, Gemini recommends placing large and repeated content at the beginning of the prompt.

We can see the number of tokens that were cache hits in the usage_metadata field (which we’ll log later in our project to verify implicit caching).

We simply call the Gemini API normally when using implicit caching without any additional configurations for caching.

Here is an example where we call the Gemini API, though without streaming:

const response = await this.ai.models.generateContent({
  model: "gemini-2.5-pro",
  Contents: fullprompt,
});

In the example above, we would need to meet the requirements for implicit caching discussed earlier. If Gemini detects any cached hits, it would automatically apply the cost savings while using the automatically created cache.

In practice, as we’ll see later in our project, usually on the second or third request within short timeframes, we typically start to see implicit caching occurring.

Explicit Caching

With explicit caching, we manually create and manage caches with IDs and expiry times. Unlike implicit caching, we can improve cost savings because we manually create and use the caches.

const cache = await this.ai.caches.create({
  model: 'gemini-2.5-pro',
  config: {
    expireTime: "2025-10-15T17:13:00Z", // Uses RFC 3339 format( in this case it was set to exprire in 2 hrs)
    contents:  largeFAQ ,
    systemInstruction: "You are a helpful assistant that can answer questions about the FAQ.",
  }
});
const response = await this.ai.models.generateContent({
  model: 'gemini-2.5-pro',
  contents: "What is TaxFlow and who is it for?",
  config: {
    cachedContent: cache.name,
  }
});

By manually setting the expiry time for our cache and using it to generate our content, we can save costs through caching.

For our FAQ chatbot, we will use implicit caching because it’s free and easy to set up.

Project Setup

Run the commands below in your terminal to create a NestJS project:

nest new faq-chatbot
cd faq-chatbot

Next, run the command below to install our dependencies:

npm i @google/genai @nestjs/config rxjs

Here, @google/generative-ai is the Gemini SDK, @nestjs/config is used for importing environment variables into our app, and rxjs is used for handling reactive streams. We’ll use it later in our project.

Next, create a .env file and paste your Google API key and the model we’ll be using:

GEMINI_API_KEY=your_api_key
GEMINI_MODEL=gemini-2.5-flash

Now, let’s create the chat module with the following structure:

src/
├── chat/
│   ├── chat.controller.spec.ts
│   ├── chat.controller.ts
│   ├── chat.module.ts
│   ├── chat.service.spec.ts
│   ├── chat.service.ts
│   └── faqs.json

For the faqs.json data, copy the code below and add about 26 more. You should aim to have around 950 words in your faqs.json file to meet the minimum token count of 1,024 for context caching when using Gemini 2.5 Flash.

[
    { "q": "What pricing plans are available?",
      "a": "Starter ($19/mo), Growth ($49/mo), and Team ($99/mo). Annual billing saves 20%. Team includes priority support and advanced permissions." },
    { "q": "Is my data secure?",
      "a": "Yes. We follow SOC 2 aligned practices and conduct periodic third-party assessments." },
    { "q": "Do you integrate with accounting software like QuickBooks or Xero?",
      "a": "Yes. We provide one-way and two-way sync with QuickBooks Online and Xero for accounts, transactions, and categories where supported." },
    { "q": "How does TaxFlow calculate estimated quarterly taxes?",
      "a": "We apply safe-harbor rules using year-to-date income and expense data, factoring jurisdiction-specific rates, thresholds, and credits where applicable." },
]

In order to import the faqs.json file into our service, we’ll need to update our tsconfig.json file.

Update it with the following:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2023",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "sourceMap": true,
    "baseUrl": "./",
    "incremental": true,
    "strictNullChecks": true,
    "forceConsistentCasingInFileNames": true,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "noFallthroughCasesInSwitch": false
  }
}

We’ve turned on JSON imports and other helpful configurations.

Next, update the app module to import the ConfigModule so that we can load environment variables in our app:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ChatModule } from './chat/chat.module';

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), ChatModule],
})
export class AppModule {}

Building the Chat Service

Our chat service takes the user query and builds a prompt with it, using the FAQ data as a prefix and providing Gemini instructions on how to reply. It then streams the reply and logs usage data metrics for tracking token usage and caching.

Update the chat.service.ts file with the following:

import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleGenAI, GenerateContentResponseUsageMetadata, GenerateContentResponse, Part } from '@google/genai';
import FAQ from './faqs.json';

@Injectable()
export class ChatService {
  private readonly ai: GoogleGenAI;
  private readonly model: string;
  private readonly logger = new Logger(ChatService.name);

  constructor(private readonly cfg: ConfigService) {
    this.ai = new GoogleGenAI({ apiKey: this.cfg.getOrThrow('GEMINI_API_KEY') });
    this.model = this.cfg.getOrThrow('GEMINI_MODEL');
  }

  async *streamAnswer(userQuestion: string): AsyncGenerator<string> {
    try {
      const fullPrompt = this.buildFullPrompt(userQuestion);
      const response = await this.callGeminiStream(fullPrompt);
      yield* this.processStreamEvents(response);
    } catch (error) {
      this.logger.error('Stream error:', error);
      yield `Error: Unable to process your question. Please try again or contact support.`;
    }
  }

  private buildFullPrompt(userQuestion: string): string {
    const systemInstruction = this.buildSystemInstruction();
    const faqBlock = this.buildFaqBlock();

    return `${systemInstruction}

    # FAQ (ground truth)
    ${faqBlock}

    User question: ${userQuestion}

    Instructions:
    - Answer strictly from the FAQ.
    - If the answer is not in the FAQ, say: "I don't know--please contact support."
    - Be concise and factual.`;
  }

  private buildSystemInstruction(): string {
    return `You are "TaxFlow Assistant", a concise FAQ chatbot for a tax management SaaS.
  Answer ONLY using the provided FAQ content.
  If the answer is not present, respond exactly: "I don't know--please contact support."

  FORMATTING REQUIREMENTS:
  - Use clear headings with **bold text** for main topics
  - Use simple bullet points with - for lists (format: - Item text)
  - Use proper line breaks between sections
  - Keep responses well-structured and easy to read
  - Use short paragraphs or bullet points when appropriate.`;
  }

  private buildFaqBlock(): string {
    return FAQ.map((item, i) => `Q${i + 1}: ${item.q}\nA${i + 1}: ${item.a}`).join('\n\n');
  }

  private async callGeminiStream(fullPrompt: string): Promise<AsyncGenerator<GenerateContentResponse>> {
    return await this.ai.models.generateContentStream({
      model: this.model,
      contents: fullPrompt,
      config: {
        temperature: 0.2,
        maxOutputTokens: 2048
      }
    });
  }

  private async *processStreamEvents(response: AsyncGenerator<GenerateContentResponse>): AsyncGenerator<string> {
    let lastUsage: GenerateContentResponseUsageMetadata | null = null;
    let sawText = false;

    for await (const fragment of response) {
      if (fragment.text) {
        sawText = true;
        yield fragment.text;
      }
      if (fragment.usageMetadata) lastUsage = fragment.usageMetadata;
    }

    this.logUsageMetadata(lastUsage);

    if (!sawText) {
      yield `I don't know--please contact support.`;
    }
  }

  private logUsageMetadata(lastUsage: GenerateContentResponseUsageMetadata | null): void {
    if (lastUsage) {
      const { promptTokenCount, totalTokenCount, cacheTokensDetails } = lastUsage;
      this.logger.log(`usage: prompt=${promptTokenCount}, total=${totalTokenCount}, cached Tokens=${cacheTokensDetails?.[0].tokenCount}`);
    }
  }
}

In the code above, streamAnswer() first builds the full prompt with the user’s question, incorporating our defined system instructions, formatting guidelines and converting the faqs.json file to a string to use as the prefix. Then, it calls Gemini with the prompt and passes the response to processStreamEvents().

When we call the Gemini service, we set the temperature to 0.2, which makes it less creative in its responses—ideal for a FAQ chatbot. We also set the maximum output tokens to 2,048 to prevent our replies from being truncated.

In our ChatService, streamAnswer() is an async generator. An async generator is a function declared with async function* that yields values over time. Think of it as an ongoing live feed—while each fragment comes one after another, they might arrive at different intervals.

In our case, streamAnswer() yields each response fragment to the controller once it’s ready, which then converts it to SSE (Server-Sent Events).

The yield keyword is used to emit values from a generator, while yield* is used to delegate to another generator, forwarding all of its yielded values. streamAnswer() uses yield* this.processStreamEvents(response) to automatically pass each of its yielded fragments to the controller, meaning that every fragment processed from Gemini by processStreamEvents() is immediately passed to the controller once it’s available. So streamAnswer() is delegating the job of producing actual output to processStreamEvents().

In processStreamEvents(), each fragment is processed once it is obtained from Gemini using:

for await (const fragment of response) {
  if (fragment.text) {
    sawText = true;
    yield fragment.text;
  }
  if (fragment.usageMetadata) lastUsage = fragment.usageMetadata;
}

Moving on, for await (const fragment of response) {...} means start iterating over the async generator called response, and once a chunk is available, call it fragment and process it. For each fragment, we check if it has text; if it does, we yield it, and if it has usageMetadata, we save it.

Here, our loop is pull-based, meaning, we request each chunk one after another as they become available. We’ll contrast this later in the controller with observables, which are push-based.

The usageMetadata object includes metadata from Gemini, such as promptTokenCount, which represents the total input tokens for the prompt we sent; totalTokenCount, which includes both input and output tokens used for the request and response; and cacheTokensDetails, which contains the cached token count and is undefined unless Gemini finds cached hits. We log all of these details.

Setting Up the Controller

Next, let’s set up our controller with SSE, update your chat.controller.ts file with the following:

import { Controller, Query, Sse } from '@nestjs/common';
import { from, map, Observable, catchError, of } from 'rxjs';
import { ChatService } from './chat.service';

@Controller('chat')
export class ChatController {
    constructor(private readonly chatService: ChatService) {}

    @Sse('stream')
  stream(@Query('q') q: string): Observable<MessageEvent<string>> {
    const asyncGenerator = this.chatService.streamAnswer(q ?? '');
    return from(asyncGenerator).pipe(
      map(piece => ({ data: piece }) as MessageEvent<string>),
      catchError(error => {
        console.error('Controller stream error:', error);
        return of({ data: 'Error: Unable to process your question. Please try again.' } as MessageEvent<string>);
      })
    );
  }
}

SSE is a one-way HTTP stream from server to browser. In NestJS, our server pushes messages as they become available in the form of Observable<MessageEvent>, and we use @Sse() to decorate our route as a Server-Sent Events endpoint.

The async generator returned by streamAnswer is converted into an observable using the from() method from RxJS. An observable, like an async generator, represents a stream of values over time.

However, an async generator is pull-based, with the consumer requesting each value as it becomes available, whereas an observable is push-based, with the producer sending new values to all subscribers as they are emitted.

Next, map(piece => ({ data: piece })) wraps each chunk as a MessageEvent so NestJS can send it as a Server-Sent Event.

Finally, catchError(...) handles errors by pushing a friendly message instead of breaking the stream if an error occurs.

Testing the Chatbot

With everything set up, it’s time to start our server and test our chatbot. Try out the following cURL requests and observe the responses and the logs. You should notice that by the second or third request, the cached token count begins to appear in the logs, indicating that implicit caching has kicked in.

curl -N -G "http://localhost:3000/chat/stream" \
  -H "Accept: text/event-stream" \
  --data-urlencode "q=What pricing plans are available?"
curl -N -G "http://localhost:3000/chat/stream" \
  -H "Accept: text/event-stream" \
  --data-urlencode "q=Is my data secure?"
curl -N -G "http://localhost:3000/chat/stream" \
  -H "Accept: text/event-stream" \
  --data-urlencode "q=What integrations do you support?"
curl -N -G "http://localhost:3000/chat/stream" \
  -H "Accept: text/event-stream" \
  --data-urlencode "q=How does TaxFlow calculate estimated quarterly taxes?"

With everything set up correctly, your logs should look similar to this, showing implicit caching occurring.

Server logs example

Your responses should look like this, with chunks coming one after another, showing the nature of streaming.

Example showing response

Conclusion

In this post, we built a real-time FAQ chatbot for a tax management SaaS using Gemini 2.5 Flash and NestJS with SSE to stream replies to the user. Throughout this project, you’ve learned how to set up Gemini and now understand how context caching in Gemini works. Possible next steps include implementing explicit caching, implementing RAG with embeddings for larger FAQ datasets, and exploring Google’s Search grounding for live data.

Read the whole story
alvinashcraft
15 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Discover the 2026 Dates For Google I/O and Microsoft Build

1 Share
Read the whole story
alvinashcraft
25 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Fun Five Year Retrospective on Marten Adoption

1 Share

In the midst of some let’s call it “market” research to better understand how Marten stacks up to a newer competitor, I stumbled back over a GitHub discussion I initiated in 2021 called “What would it take for you to adopt Marten?” long before I was able to found JasperFx Software. I seeded this original discussion with my thoughts about the then forthcoming giant Marten 4.0 release that was meant to permanently put Marten on a solid technical foundation for all time and address all known significant technical shortcomings of Marten.

Narrators’s voice: the V4 release was indeed a major step forward and still shapes a great deal of Marten’s internals, but it was not even remotely the end all, be all of technical releases and V5 came out less than 6 months later to address shortcomings of V4 and to add first class multi-tenancy through separate databases. And arguably, V7 just three years later was nearly as big a change to Marten’s internals.

So now that it’s five years later and Marten’s usage numbers are vastly greater than that moment in time in 2021, let me run through the things we thought needed to change to garner more usage, whether or not and how those ideas took fruit, and whether or not I think those ideas made any difference in the end.

Enterprise Level Support

People frequently told me that they could not seriously consider Marten or later Wolverine without there being commercial support for those tools or at least a company behind them. As of now, JasperFx Software (my company) provides support agreements for any tool under the JasperFx GitHub organization. I would say though that the JasperFx support agreement ends up being more like an ongoing consulting engagement rather than the “here’s an email for support, we’ll response within 72 hours” licensing agreement that you’d be getting from other Event Driven Architecture tools and companies in the .NET space.

Read more about that in Little Diary of How JasperFx Helps Our Clients.

And no, we’re not a big company at all, but we’re getting there and at least “we” isn’t just the royal “we” now:)

I’m hoping that JasperFx is able to expand when we are able to start selling the CritterWatch commercial add on soon.

More Professional Documentation

Long story short, a good, a modern looking website for your project is an absolute must. Today, all of the Critter Stack / JasperFx projects use VitePress and MarkdownSnippets for our documentation websites. Plus we have real project logo images that I really like myself created by Khalid Abuhakmeh. Babu Annamalai did a fantastic job on setting up our documentation infrastructure.

People do still complain about the documentation from time to time, but after I was mercilessly flogged online for the StructureMap documentation being so far behind in the late 00’s and FubuMVC never really having had any, I’ve been paranoid about OSS documentation ever since and we as a community try really hard to curate and expand our documentation. Anne Erdtsieck especially has added quite a bit of explanatory detail to the Marten documentation in the last six months.

It’s only anecdotal evidence, but the availability of the LLMS-friendly docs plus the most recent advances in AI LLM tools seem to have dramatically reduced the amount of questions we’re fielding in our Discord chat rooms while our usage numbers are still accelerating.

Oh, and I cannot emphasize more how important and valuable it is to be able to both quickly publish documentation updates and to enable users to quickly make suggestions to the documentation through pull requests.

Moar YouTube Videos

I dragged my feet on this one for a long time and probably still don’t do well enough, but we have the JasperFx Software Channel now with some videos and plenty of live streams. I’ve had mostly positive feedback on the live streams, so it’s just up to me to get back in a groove on this one.

SQL Server or CosmosDb Support in Addition to PostgreSQL

The most common complaint or concern about Marten in its first 5-7 years was that it only supported PostgreSQL as a backing data store. The most common advice we got from the outside was that we absolutely had to have SQL Server support in order to be viable inside the .NET ecosystem where shops do tend to be conservative in technology adoption and also tend to favor Microsoft offerings.

While I’ve always seen the obvious wisdom in supporting SQL Server, I never believed that it was practical to replicate Marten’s functionality with SQL Server. Partially because SQL Server lagged far behind PostgreSQL in its JSON capabilities for a long time and partially just out of sheer bandwidth limitations. I think it’s telling that nobody built a truly robust and widely used event store on top of SQL Server in the mean time.

But it’s 2026 and the math feels very different in many ways:

  1. PostgreSQL has grown in stature and at least in my experience, far more .NET shops are happy to take the plunge into PostgreSQL. It absolutely helps that the PostgreSQL ecosystem has absolutely exploded with innovation and that PostgreSQL has first class managed hosting or even serverless support on every cloud provider of any stature.
  2. SQL Server 2025 introduced a new native JSON type that brought SQL Server at least into the same neighborhood as PostgreSQL’s JSONB type. Using that, the JasperFx Software is getting close to releasing a full fledged port of most of Marten (you won’t miss the parts that were left out, I know I won’t!) called “Polecat” that will be backed by SQL Server 2025. We’ll see how much traction that tool gets, but early feedback has been positive.
  3. While we’re not there yet on Event Sourcing, at least Wolverine does have CosmosDb backed transactional inbox and outbox support as well as other integration into Wolverine handlers. I don’t have any immediate plans for Event Sourcing with CosmosDb other than “wouldn’t that be nice?” kind of thoughts. I don’t hear that many requests for this. I get even less feedback about DynamoDb, but I’ve always assumed we’d get around to that some day too.

Better Support for Cross Document Views or Queries

So, yeah. Document database approaches are awesome when your problem domain is well described by self-contained entities, but maybe not so much if you really need to model a lot of relationships between different first class entities in your system. Marten already had the Include() operator in our LINQ support, but folks aren’t super enthusiastic about it all the time. As Marten really became mostly about Event Sourcing over time, some of this issue went away for folks who could focus on using projections to just write documents out exactly as your use cases needed — which can sometimes happily eliminate the need for fancy JOIN queries and AutoMapper type translation in memory. However, I’ve worked with several JasperFx clients and other users in the past couple years who had real struggles with creating denormalized views with Marten projections, so that needed work too.

While that complaint was made in 2021, we now have or are just about to get in the next wave of releases:

  • The new “composite projection” model that was designed for easier creation of denormalized event projection views that has already met with some early success (and actionable feedback). This feature was also designed with some performance and scalability tuning in mind as well.
  • The next big release of Marten (8.23) will include support for the GroupJoin LINQ provider. Finally.
  • And let’s face it, EF Core will always have better LINQ support than Marten over all and a straight up relational table is probably always going to be more appropriate for reporting. To that end, Marten 8.23 will also have an extension library that adds first class event projections that write to EF Core.

Polecat 1.0 will include all of these new Marten features as well.

Improving LINQ Support

LINQ support was somewhat improved for that V4 release I was already selling in 2021, but much more so for V7 in early 2024 that moved us toward using much more PostgreSQL specific optimizations in JSONB searching as we were able to utilize JSONPath searching or back to the PostgreSQL containment operator.

At this point, it has turned out that recent versions of Claude are very effective at enhancing or fixing issues in the LINQ provider and at this point we have zero open issues related to LINQ for the first time since Marten’s founding back in 2015!

There’s one issue open as I write this that has an existing fix that hasn’t been committed to master yet, so if you go check up on me, I’m not technically lying:)

Open Telemetry Support and other Improved Metadata

Open Telemetry support is table stakes for .NET application framework tools and especially for any kind of Event Driven Architecture or Event Sourcing tool like Marten. We’ve had all that since Marten V7, with occasional enhancements or adjustments since in reaction to JasperFx client needs.

More Sample Applications

Yeah, we could still do a lot better on this front. Sigh.

One thing I want to try doing soon is developing some Claude skills for the Critter Stack in general, and a particular focus on creating instructions for best practices converting codebases to the Critter Stack. As part of that, I’ve identified about a dozen open source sample applications out there that would be good targets for this work. It’s a lazy way to create new samples applications while building an AI offering for JasperFx, but I’m all about being lazy sometimes.

We’ll see how this goes.

Scalability Improvements including Sharding

We’ve done a lot here since that 2021 discussion. Some of the Event Sourcing scalability options are explained here. This isn’t an exhaustive list, but since 2021 we have:

  • Much better load distribution of asynchronous projection and subscription work within clusters
  • Support for PostgreSQL read replicas
  • First class support for managing PostgreSQL native partitioning with Marten
  • A ton of internal improvements including work to utilize the latest, greatest low level support in Npgsql for query batching

And for that matter, I’ve finishing up a proposal this weekend for a new JasperFx client looking to scale a single Marten system to the neighborhood of 200-300 billion events, so there’s still more work ahead.

Event Streaming Support or Transactional Outbox Integration

This was frequently called out as a big missing feature in Marten in 2021, but with the integration into the full Critter Stack with Wolverine, we have first class event streaming support that was introduced in 2024 for every messaging technology that Wolverine supports today, which is just about everything you could possibly think to use!

Management User Interface

Sigh. Still in flight, but now very heavily in flight with a CritterWatch MVP promised to a JasperFx client by the end of this month. Learn more about that here:

Cloud Hosting Models and Recipes

People have brought this up a bit over the years, but we don’t have much other than some best practices with using Marten inside of Docker containers. I think it helps us that PostgreSQL is almost ubiquitous now and that otherwise a Marten application is just a .NET application.



Read the whole story
alvinashcraft
43 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

When Read­Directory­ChangesW reports that a deletion occurred, how can I learn more about the deleted thing?

1 Share

A customer was using Read­Directory­ChangesW to monitor changes to a directory. However, they ran into a problem when they received a FILE_ACTION_REMOVED notification: Since the notification is raised when the item is deleted, they can’t do a Get­File­AttributesEx to find out whether the deleted item was a file or a subdirectory, and if it was a file, the size of the deleted file. Their program needs that information as part of its directory monitoring, so what mechanism is there to recover that information?

The Read­Directory­ChangesW function provides no way to recover information about the item that was deleted. All you get is the name of the item.

Recall that Read­Directory­ChangesW is for detecting changes to information that would appear in a directory listing. The idea is that your program performs a Find­First­File/Find­Next­File to build an in-memory cache for a directory, and then you can use Read­Directory­ChangesW to perform incremental updates to your cache. For example, if you see a FILE_ACTION_ADDED, then you can call Get­File­Attributes or Get­File­Attributes­Ex to get information about the thing that was added and update your cache. That way, when you see the FILE_ACTION_REMOVED, you can read the entry from your cache to get the information about the item that was removed (as well as removing it from your cache).

There is a race condition here, however. If the item is added and then immediately deleted, then when you get around to calling Get­File­Attributes, it won’t be there, so you don’t actually know what it was.

Fortunately, there’s Read­Directory­Changes­ExW. If you ask for Read­Directory­Notify­Extended­Information, then you get back a series of FILE_NOTIFY_EXTENDED_INFORMATION structures, and in addition to the action and the file name, those also contain directory information about the item, including its file attributes. This information is provided both on the add and on the remove, so you can just look at the FileAttributes on the FILE_ACTION_REMOVED to see whether it was a file or a folder, and if it was a file, you can use the FileSize to see the logical size of the file at the time it was deleted.

The post When <CODE>Read­Directory­ChangesW</CODE> reports that a deletion occurred, how can I learn more about the deleted thing? appeared first on The Old New Thing.

Read the whole story
alvinashcraft
52 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

When to Use Strategy Pattern in C#: Decision Guide with Examples

1 Share

When to use Strategy pattern in C#: decision criteria, code examples, and scenarios to determine if Strategy pattern is the right choice for your application.

Read the whole story
alvinashcraft
56 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Track Changes in DOCX Editor: Build a Word-Like Collaborative Review Experience

1 Share

Track Changes in DOCX Editor: Build a Word-Like Collaborative Review Experience

TL;DR: Tired of chaotic review cycles? The Syncfusion DOCX Editor brings Word‑like Track Changes to the browser, no Microsoft Word or Office interop required. Turn on real‑time tracking, scan edits in a clean Revisions Pane, accept/reject via UI or API, and lock approvals with RevisionsOnly protection. Result: faster reviews, clean formatting, and programmable control that scales.

Ever tried managing edits from multiple reviewers and ended up drowning in conflicting changes, missing comments, or broken formatting? You’re not alone. Developers and teams struggle every day with messy document workflows, unclear authorship, inconsistent styling, and approval processes that feel more chaotic than collaborative.

That’s exactly where Syncfusion® DOCX Editor transforms the experience. With Word‑like track changes built directly into the browser, you get precise control over:

  • Every insertion and deletion,
  • A clean Revisions Pane for fast navigation,
  • Author‑wise filtering,
  • API‑driven automation, and
  • Secure RevisionsOnly protection to prevent unauthorized approvals.

And best of all? It works without Microsoft Word or Office interop, making it perfect for scalable, cloud‑first, cross‑platform document review apps.

Let’s see how to integrate track changes, streamline real‑time collaboration, and build frictionless review workflows using Syncfusion’s developer‑friendly DOCX Editor.

Mastering Word documents is a breeze with the Syncfusion Word Library, simplifying every aspect of document creation and management!

What are track changes, and why do they matter?

Track changes records document modifications, including insertions and deletions, without altering the original content. Rather than replacing text, it marks edits so reviewers can see additions or removals before accepting or rejecting them. This ensures transparency and streamlines reviews.

Syncfusion DOCX Editor takes track changes beyond the basics, delivering a Word-like reviewing experience with advanced capabilities, including:

  • Tracking text additions and deletions for complete editing history.
  • Revision pane on the sidebar displaying all tracked changes for easy navigation.
  • Accept and reject options for authors and reviewers to finalize edits.
  • Author and timestamp tracking for accountability and clarity.
  • Advanced filtering based on author and mode of changes for faster review.

Getting started with Syncfusion DOCX Editor

Syncfusion makes track changes integration effortless with its lightweight, browser-based DOCX Editor. In just a few steps, you can embed a fully functional editor into your web app without heavy dependencies or complex setups.

With a built-in UI for reviewing changes, programmatic APIs for dynamic control, and cross-platform compatibility, it makes it easy to build dynamic collaborative document review apps.

Let’s deep dive into the steps to set up the DOCX Editor and enable track changes so it integrates smoothly with your workflow.

Step 1: Create a project folder

Start by creating a folder for your application, for example, app. This folder will contain all your project files, including the HTML page that will host the Syncfusion JavaScript DOCX Editor component.

Step 2: Use the global script and style from CDN

Next, add the required Syncfusion DOCX Editor scripts and styles. Since Essential JS 2 components are hosted on a CDN, you don’t need to install packages.

<head>
    <title>Essential JS 2 PDF Viewer</title>
    <!-- Common Tailwind 3 Theme -->
    <link href="https://cdn.syncfusion.com/ej2/32.1.19/tailwind3.css" rel="stylesheet">
    <!-- Essential JS 2 Script -->
    <script src="https://cdn.syncfusion.com/ej2/32.1.19/dist/ej2.min.js"></script>
</head>

Step 3: Create HTML and initialize DOCX Editor

Inside your app folder, create a index.html file. Add a <div> element to host the DOCX Editor, then initialize the Syncfusion JavaScript Document Editor component using JavaScript.

Here’s the code you need:

<body>
    <!-- Element which is going to render as Document Editor -->
    <div id='DocumentEditor' style='height:620px'>
    </div>

    <script>
        // Initialize DocumentEditorContainer component.
        let documenteditorContainer = new ej.documenteditor.DocumentEditorContainer({
            enableToolbar: true,
            height: '590px'
        });
        ej.documenteditor.DocumentEditorContainer.Inject(ej.documenteditor.Toolbar);
        documenteditorContainer.serviceUrl = 'https://document.syncfusion.com/web-services/docx-editor/api/documenteditor/';
        // DocumentEditorContainer control rendering starts
        documenteditorContainer.appendTo('#DocumentEditor');
    </script>
</body>
</html>

Note: The Web API hosted link https://document.syncfusion.com/web-services/docx-editor/api/documenteditor/ utilized in the Document Editor’s serviceUrl property is intended solely for demonstration and evaluation purposes. For production deployment, please host your own web service with your required server configurations. You can refer to and reuse the GitHub Web Service example to host your own web service, using the serviceUrl property.

Step 4: Launch in browser

Finally, open your index.html file in any modern web browser such as Chrome, Edge, or Firefox. The Syncfusion Docx Editor component will load instantly.

Integrating Syncfusion DOCX editor in a web application
Integrating Syncfusion DOCX editor in a web application

Note: For more detailed guidance for getting started with DOCX Editor, check out our official documentation.

Get insights into the Syncfusion’s Word Library and its stunning array of features with its extensive documentation.

Enable track changes for collaborative review

Tracked changes are the key to transparent and efficient document reviewing. Enabling this feature with our DOCX Editor is simple and flexible.

Enable track changes via UI

Users can enable this feature directly by clicking the Track Changes option in the built-in toolbar and start recording insertions and deletions instantly.

Refer to the following image.

Track changes option in Syncfusion DOCX Editor’s toolbar
Track changes option in Syncfusion DOCX Editor’s toolbar

Enable track changes programmatically

The DOCX Editor provides APIs for developers to programmatically enable track changes, providing control beyond built-in UI options.

You can programmatically enable track changes using the enableTrackChanges API while initializing the component or dynamically when the document changes with just a few lines of code:

let documenteditorContainer = new ej.documenteditor.DocumentEditorContainer({
    enableTrackChanges: true
});

Reviewing documents efficiently with accept and reject options

When the track changes feature is enabled in the DOCX Editor, every edit is visually marked for easy identification:

  • Strikethrough for deleted text.
  • Underline for inserted text.
Viewing tracked changes using Syncfusion DOCX Editor
Viewing tracked changes using Syncfusion DOCX Editor

These indicators help reviewers quickly spot changes and make it easy to track text throughout the document. The core functionality of reviewing lies in Accept/Reject actions, which decide which content should stay and which should be removed. Syncfusion DOCX Editor makes this process seamless with intuitive UI tools and powerful APIs, ensuring smooth approval workflows without compromising formatting.

Accept and reject changes using revision panes

Every document change is listed in the Revision Pane on the sidebar. It serves as a single place to view all tracked changes, displaying edits with clear indicators that let you accept or reject them instantly. Whether you’re preserving formatting or removing unnecessary edits, this feature ensures collaborative review is effortless, accurate, and transparent.

Revision pane in Syncfusion DOCX Editor
Revision pane in Syncfusion DOCX Editor

Unearth the endless potential firsthand through demos highlighting the features of the Syncfusion Word Library.

Accept and reject changes programmatically

Syncfusion DOCX Editor doesn’t just stop at UI-based review tools; it gives developers complete control through powerful APIs to automate your approval workflow. You can programmatically fetch the full revision collection, then accept/reject all changes at once or handle specific revisions individually with just a few lines of code, as shown below.

// Get revisions from the current document
let revisions = documenteditorContainer.documentEditor.revisions;

// Accept all tracked changes
revisions.acceptAll();

// Reject all tracked changes
revisions.rejectAll();

// Accept specific changes
revisions.get(0).accept();

// Reject specific changes
revisions.get(1).reject();

Note: For more details, refer to the track changes feature in the Docx Editor documentation.

Managing document protection in track changes mode

Collaborative reviewing is effective but requires control. Multiple reviewers can suggest changes, but only the author controls acceptance. Syncfusion’s DOCX Editor enforces this with track changes protection, ensuring transparent collaboration and preventing unauthorized approvals.

With protection enabled, all users can make suggestions, but accept/reject actions remain exclusive to the author, ensuring a secure approval process.

Enable track changes protection using restricted editing

Our DOCX Editor simplifies document review by enabling you to secure the process directly from the UI. Using the Restrict Editing option, we can select Track Changes mode and apply password protection. In this protected state, collaborators can make edits that are recorded as tracked changes, but only the author can finalize them by accepting or rejecting changes after removing protection, ensuring a controlled and secure workflow.

Here’s a preview of the feature in action:

Enabling track changes protection using the restrict editing option in the toolbar
Enabling track changes protection using the restrict editing option in the toolbar

Programmatic protection of track changes

We can also programmatically protect track changes using the enforceProtection API. We can lock the document in RevisionsOnly mode and later remove protection with the stopProtection API when needed, all with just a few lines of code.

// Enforce protection
documenteditorContainer.documentEditor.editor.enforceProtection(
    '123',
    'RevisionsOnly'
);

// Stop the document protection
documenteditorContainer.documentEditor.editor.stopProtection(
    '123'
);

Advanced track changes features for faster review

Reviewing and tracking changes in a document shouldn’t feel like searching for a needle in a haystack. With the Docx Editor’s advanced track changes features, you can streamline the entire review process and focus on what matters most.

Our built-in Review Panel empowers you to:

  • Filter changes by author or type for targeted review.
  • Navigate edits effortlessly, jump any tracked change with a single click or programmatically through the API.
  • Export the finalized document after review to multiple formats, such as SFDT, DOCX, DOTX, and plain text, for further use or archiving.

See the feature in action below:

Advanced track changes option in Syncfusion DOCX Editor
Advanced track changes option in Syncfusion DOCX Editor

Together, these capabilities make it easy to compare Word documents and track changes efficiently, delivering a fast, organized, and user-friendly experience, even for large-scale review processes.

GitHub reference

Also, check out the examples for track changes in Syncfusion Document Editor on this GitHub repository.

Frequently Asked Questions

Will accepting all changes alter the document’s original style?

No, the editor applies outcomes using the existing style map; only the chosen content and formatting persist.

Does track changes work in right‑to‑left scripts and mixed‑language documents?

Yes, revisions track runs of text, so RTL scripts and mixed locales are preserved, and review markers remain aligned.

Does Syncfusion support collaborative editing beyond track changes?

Yes, Syncfusion also supports collaborative editing commenting, section‑level protection, and workflow automation, allowing teams to collaborate more smoothly and manage reviews in a structured way.

Can I generate a finalized document that excludes all revision history?

Yes, once all tracked changes are accepted or rejected, you can export the document as a clean final version, ensuring no revision or change history remains in the output.

How does the editor resolve conflicting edits made to the same text segment?

The editor shows each conflicting edit as a separate change on the same part of the text. You can review them one by one in the revisions pane and choose which version you want to keep.

Will tracking changes work inside tables, headers, and footers?

Yes, revisions are recorded in structured regions such as tables, headers/footers, and lists, just like body text.

Can I run the DOCX backend services in Docker containers?

Yes, Syncfusion provides a ready‑to‑use Docker image called Word Processor Server that lets you easily host the DOCX Editor backend in Docker or Kubernetes

The Syncfusion Word Library provides cross-platform compatibility and can be tailored to your unique needs.

Start building smarter collaborative review workflows today

Thanks for reading! Collaborative document reviews don’t have to feel chaotic. With features like real‑time track changes, a clean revisions pane, author‑based filtering, and secure approval workflows, the Syncfusion DOCX Editor SDK gives you everything you need to build fast, transparent review experiences your users will love.

Whether you’re working on legal documents, technical specs, business proposals, or internal reports, you can deliver consistent formatting, reliable tracking, and a smooth end‑to‑end workflow, all directly inside your web app. Try it out today and discover what smoother, smarter document collaboration actually feels like.

If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forumsupport portal, or feedback portal for queries. We are always happy to assist you!

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories