Tutorials & Guides
How to Build a Realistic Mock API Server Using Gemini Flash Structured Outputs and Fastify
Static mock servers are brittle and unrealistic. Build a dynamic, stateful mock API server that behaves like a real production database using Gemini Flash and Fastify.
Updated 9/4/2026
Frontend development is often held hostage by the backend. You want to build a feature, but the API endpoints aren't ready, or the staging database is completely empty.
Most developers solve this by spin-firing a mock API server using toolsets that return static JSON files. But static mock data is lifeless. It does not persist changes. If you send a POST request to create a new user, that user doesn't show up in subsequent GET /users calls. If you try to update a resource with invalid data types, the mock server accepts it blindly without validation.
We can build a dynamic, stateful mock API server that behaves exactly like a real production database, complete with state persistence, validation, and realistic data generation. Best of all, we do not need to build a database or write hundreds of lines of mock handlers. We can use Fastify combined with the structured outputs of /platforms/gemini to act as our intelligent, database-less backend.
The Concept: LLM as an In-Memory Database State Machine
Instead of writing complex mock handlers for every single API endpoint, we will keep a simple, local, in-memory state array inside our Fastify server.
When a request comes in (whether it's a GET, POST, PUT, or DELETE), we will bundle three things and send them to Gemini Flash:
1. The current in-memory database state.
2. The incoming HTTP request (method, route, headers, and body).
3. The target JSON schema we expect the API to return.
Gemini Flash will process the request, update the database state, and return the new state along with the appropriate HTTP status code and response payload. We then update our local memory and send the response back to the client. Because Gemini Flash is incredibly cheap and fast, our mock server remains snappy and responsive.
Step 1: Setting Up the Fastify Project
First, initialize a new Node.js project and install the required dependencies. We will use Fastify for our HTTP server and the official Google Gen AI SDK.
`bash
mkdir gemini-mock-server
cd gemini-mock-server
npm init -y
npm install fastify @google/genai dotenv
`
Create a .env file in the root of your project and add your Gemini API key:
`text
GEMINI_API_KEY=your_api_key_here
`
If you run into issues obtaining or setting up your key, refer to the Gemini Support Site for help.
Step 2: Defining the Mock Schema and State
We will define our database state as a simple JSON object containing collections of resources. Let's build a mock server for a task management application with users and tasks collections.
Create a file named server.js and add the setup boilerplate:
`javascript
import Fastify from 'fastify';
import { GoogleGenAI, Type } from '@google/genai';
import dotenv from 'dotenv';
dotenv.config();
const fastify = Fastify({ logger: true }); const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
// Our in-memory database state
let dbState = {
users: [
{ id: "usr_1", name: "Alice Smith", email: "alice@example.com" },
{ id: "usr_2", name: "Bob Jones", email: "bob@example.com" }
],
tasks: [
{ id: "tsk_1", userId: "usr_1", title: "Implement OAuth flow", status: "IN_PROGRESS" }
]
};
`
Step 3: Implementing the Structured Output Schema
To ensure Gemini Flash returns data our Fastify server can parse reliably, we must define a strict response schema using Gemini's structured output. We want the model to return two things: the updated database state and the HTTP response (including status code and JSON body).
Let's write out the Gemini schema definitions:
`javascript
const mockEngineResponseSchema = {
type: Type.OBJECT,
properties: {
updatedDbState: {
type: Type.OBJECT,
properties: {
users: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
id: { type: Type.STRING },
name: { type: Type.STRING },
email: { type: Type.STRING }
},
required: ["id", "name", "email"]
}
},
tasks: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
id: { type: Type.STRING },
userId: { type: Type.STRING },
title: { type: Type.STRING },
status: { type: Type.STRING }
},
required: ["id", "userId", "title", "status"]
}
}
},
required: ["users", "tasks"]
},
httpResponse: {
type: Type.OBJECT,
properties: {
statusCode: { type: Type.INTEGER },
body: {
type: Type.OBJECT,
description: "The JSON payload to return to the client. Must match standard REST API conventions."
}
},
required: ["statusCode", "body"]
}
},
required: ["updatedDbState", "httpResponse"]
};
`
For a deeper look into how schemas shape model responses, check out our /glossary definition of Structured Outputs.
Step 4: Writing the Catch-All Route Handler
Instead of defining separate routes for every single endpoint, we can use a wildcard route in Fastify to capture all requests and pipe them directly into Gemini Flash.
`javascript
fastify.route({
method: ['GET', 'POST', 'PUT', 'DELETE'],
url: '/*',
handler: async (request, reply) => {
const { method, url, body, headers } = request;
const prompt = `
You are acting as an API engine and an in-memory database resolver.
CURRENT DATABASE STATE:
${JSON.stringify(dbState, null, 2)}
INCOMING HTTP REQUEST:
Method: ${method}
Path: ${url}
Body: ${body ? JSON.stringify(body) : 'None'}
Headers: ${JSON.stringify(headers)}
YOUR TASKS:
1. Process the incoming request against the current database state.
2. If it is a GET request, retrieve the records. Return a 404 if a specific ID is not found.
3. If it is a POST, PUT, or DELETE request, modify the database state correctly. Ensure you auto-generate UUID style IDs for new resources.
4. Validate fields. For example, if a task is added with an invalid userId, return a realistic 400 Bad Request error payload inside 'httpResponse.body' and do not modify the database state.
5. Return the updated database state and the realistic API response.
`;
try { const response = await ai.models.generateContent({ model: 'gemini-1.5-flash', contents: prompt, config: { responseMimeType: 'application/json', responseSchema: mockEngineResponseSchema, temperature: 0.1 // Low temperature ensures consistent database behaviour } });
const result = JSON.parse(response.text); // Persist the updated state in memory dbState = result.updatedDbState; // Send the response back to the client return reply .status(result.httpResponse.statusCode) .send(result.httpResponse.body); } catch (error) { fastify.log.error(error); return reply.status(500).send({ error: "Failed to resolve mock state transition." }); } } });
// Start the server
const start = async () => {
try {
await fastify.listen({ port: 3000 });
console.log('Mock API server listening on http://localhost:3000');
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
`
Step 5: Testing the Dynamic Mock Server
Start your mock server with node server.js. Now, open up a terminal and test it out with some local curl requests.
First, query all tasks:
`bash
curl http://localhost:3000/tasks
`
Response:
`json
[
{ "id": "tsk_1", "userId": "usr_1", "title": "Implement OAuth flow", "status": "IN_PROGRESS" }
]
`
Now, let's create a new task using a POST request:
`bash
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"userId": "usr_2", "title": "Write API docs", "status": "TODO"}'
`
Response:
`json
{ "id": "tsk_2", "userId": "usr_2", "title": "Write API docs", "status": "TODO" }
`
If you query the /tasks route again, you'll see the state has persisted!
What happens if we send a bad request, like trying to create a task for a user that doesn't exist?
`bash
curl -X POST http://localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"userId": "non_existent_user", "title": "Ghost Task", "status": "TODO"}'
`
Response:
`json
{ "error": "User with ID non_existent_user does not exist." }
`
(With an HTTP Status Code: `400 Bad Request`)
Elevating Your Frontend Prototyping
Because Gemini Flash is lightning-fast, you can prototype entire complex workflows—like adding items to a cart, deleting users, or filtering tasks—without writing a single line of backend database code. You get realistic edge cases, error messages, and dynamic state transitions out of the box, letting you focus entirely on crafting a stellar frontend UI.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.