Skip to main content

Command Palette

Search for a command to run...

How to Integrate Generative AI in Flutter Using a NestJS Backend with Gemini

Updated
6 min read
How to Integrate Generative AI in Flutter Using a NestJS Backend with Gemini

Many tutorials show how to “use AI in Flutter” by calling an API directly from the app. That approach works for prototypes, but in real production environments, it brings several problems: exposed API keys, no rate limiting, no control over costs, and no ability to enforce business rules or version models over time.

A better architecture — the one I actually use in my own projects — is:

Flutter → NestJS backend → Gemini API

This gives you security, observability, and full control of your AI layer.

In this article, I will walk you through the whole architecture and provide complete NestJS + Flutter code you can copy and use in your own apps.

Why You Should NOT Call Gemini Directly from Flutter

Before we start, you might be wondering why it's not a good idea to use the Gemini API key directly in your Flutter applications.

Calling Gemini (or any AI provider) straight from the mobile client creates several critical problems:

  • Your API key will be exposed in case anyone reverse engineers your app.

  • No rate limiting to protect your bill.

  • No control over quotas, logs, or usage patterns.

  • No way to enforce business rules (max prompt size, allowed features, etc.).

  • You must ship new app versions if you want to change models or parameters.

Just to mention some reasons, this is why the correct architecture always includes a backend. NestJS sits between your Flutter app and Gemini, handling authentication, validation, logging, safeguards, rate limiting, and model management — while keeping your API keys 100% secure.

High-Level Architecture

NestJS becomes the “AI gateway” for all your apps — secure, flexible, and future-proof.

Before we start, please make sure you get a Gemini API key. You can get one from this page: https://aistudio.google.com/api-keys

Remember to create a Google Cloud Project before creating a new API Key.

1. Creating an AI Module in NestJS

Inside your NestJS project, we're gonna create a new module using NestJS CLI. On your terminal, put the following:

nest g module gemini

This will give us a new Module called Gemini.

src/gemini/gemini.module.ts
src/gemini/gemini.service.ts
src/gemini/gemini.controller.ts

NestJS CLI is a really cool tool that lets you create NestJS code really fast. This is one of the features I love most about Nest.

There are three key files: the Module, the Service, and the Controller. The Module acts as the orchestrator, wiring the controllers and providers for this Gemini Feature. The Gemini Controller communicates with the Gemini API through a use case that we will implement later on. And finally, the Gemini Service exposes the endpoints that our Flutter application will interact with.

gemini.module.ts

import { Module } from '@nestjs/common';
import { GeminiService } from './gemini.service';
import { GeminiController } from './gemini.controller';

@Module({
  controllers: [GeminiController],
  providers: [GeminiService],
})
export class GeminiModule {}

2. NestJS Service to Call Gemini

We're gona use @google/genai package. You can read the documentation in here for more information: https://ai.google.dev/gemini-api/docs For a better structure, we are gonna create a use case file as well. We have the following:

gemini.service.ts

import { Injectable } from '@nestjs/common';

import { GoogleGenAI } from '@google/genai';
import { geminiUseCase } from './use-cases/gemini.use-case';
import { GeminiPromptDto } from './dtos/gemini-promot.dto';

@Injectable()
export class GeminiService {
  private ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  async geminiPrompt(geminiPromptDto: GeminiPromptDto) {
    return geminiUseCase(this.ai, geminiPromptDto);
  }
}

gemini.use-case.ts

import { GoogleGenAI } from '@google/genai';
import { GeminiPromptDto } from '../dtos/gemini-promot.dto';

export const geminiUseCase = async (
  ai: GoogleGenAI,
  basicPromptDto: GeminiPromptDto,
) => {
  const response = await ai.models.generateContent({
    model: 'gemini-2.5-flash',
    contents: basicPromptDto.prompt,
    config: {
      systemInstruction: `
      Responde únicamente en español
      En formato markdown
      Usa negritas de esta forma __
      Usa el sistema métrico decimal
      `,
    },
  });

  return response.text;
};

Add your key to .env :

GEMINI_API_KEY=your_key_here

3. Exposing a Clean Endpoint for Flutter

gemini.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { GeminiService } from './gemini.service';
import { GeminiPromptDto } from './dtos/gemini-promot.dto';

@Controller('gemini')
export class GeminiController {
  constructor(private readonly geminiService: GeminiService) {}

  @Post('generate')
  basicPrompt(@Body() geminiPromptDto: GeminiPromptDto) {
    return this.geminiService.geminiPrompt(geminiPromptDto);
  }
}

Now you have:

POST /gemini/generate
{
  "prompt": "Write a product description for a tip calculator app."
}

4. Recommend Protections (Very Important in Production)

To operate AI safely:

Rate limiting:

import { Throttle } from '@nestjs/throttler';

@Throttle(5, 60) // 5 requests per minute

Validation Pipe

Prevent sending empty or malicious prompts.

API key or auth in your backend

Even a simple auth token adds a security layer.

Logging + token usage

Helps detect abuse and control monthly billing.

5. Calling NestJS from Flutter Using Dio

Here's a clean service class in Flutter:

ai_service.dart

import 'package:dio/dio.dart';

class AiService {
  final dio = Dio(BaseOptions(baseUrl: 'https://your-backend.com'));

  Future<String> generate(String prompt) async {
    try {
      final body = {'prompt': prompt};
      final response = await _dio.post('/gemini/generate', data: jsonEncode(body));
      return response.data;
    } catch (e) {
      return 'Error: $e';
    }
  }

6. Simple Flutter UI Example

chat_screen.dart

import 'package:flutter/material.dart';
import 'ai_service.dart';

class ChatScreen extends StatefulWidget {
  const ChatScreen({super.key});

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> {
  final controller = TextEditingController();
  final ai = AiService();
  String output = '';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Gemini + Flutter')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(controller: controller),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                final text = await ai.generate(controller.text);
                setState(() {
                  output = text;
                });
              },
              child: const Text('Generate'),
            ),
            const SizedBox(height: 16),
            Text(output),
          ],
        ),
      ),
    );
  }
}

7. My Own Experience Using This Structure

In one of my recent projects, I'm following this exact architecture. It allows me to:

  • Auto-generate text on a UI interface
  • Detect objects in user-uploaded photos using Gemini Vision
  • Generate suggestions, summaries, and metadata
  • Run lightweight agents for business logic

Flutter’s role is simpler: it just sends some input, whether text or an image.

NestJS handles everything else — calling Gemini safely, cleaning the data, and returning structured results.

The app feels lighter and faster, and you don't worry about exposing keys or AI logic in the client.

8. Some Good Practices for Production AI

Here are a few things that can save you some headaches in production:

  • Put a limit on how big prompts can be
  • Cache common or repeated AI responses
  • Track input/output tokens so you don’t get billing surprises
  • Version your models as you upgrade (gemini-pro-1.5 → 2.0, etc.)
  • Handle loading states properly — AI can take a moment
  • Keep every model parameter and key on the server, never in the app

Conclusion

So there you have it. Using Flutter + NestJS + Gemini behind a backend isn’t just a “nice-to-have” architecture — it’s a solid, production-ready setup with real advantages:

  • Your keys and logic never reach the mobile app
  • You can change models or parameters instantly from your backend without deploying a new version of your app
  • You control rate limits and usage because you have more control over your API usage
  • One backend can power multiple apps or platforms

Thank you so much for reading this far, and let me know if you have any questions in the comment section.

Happy Coding. :D