Understanding Generated Code

Deep dive into what DataBridge generates and how it all works together

Understanding Generated Code

Learn what DataBridge creates when you run databridge generate and how each piece fits together.

Overview

When you run databridge generate, DataBridge analyzes your Prisma schema and generates:

  • OpenAPI 3.0 Specification - Industry-standard API documentation
  • Swagger UI Plugin - Interactive API testing at /docs
  • ✅ Fastify API routes with full CRUD operations
  • ✅ Zod validation schemas
  • ✅ Prisma plugin for database access
  • ✅ Server configuration with auto-port selection
  • ✅ Test scripts for validation
  • ✅ TypeScript type definitions

Learn more: OpenAPI & Swagger UI Tutorial


File Structure

After running databridge generate in a project, you’ll have:

my-project/
├── apps/
│   └── api/
│       └── src/
│           ├── openapi.json           # OpenAPI 3.0 specification
│           ├── routes/
│           │   ├── _generated.ts      # Route registry
│           │   ├── users.ts           # User CRUD routes
│           │   ├── products.ts        # Product CRUD routes
│           │   └── orders.ts          # Order CRUD routes
│           ├── plugins/
│           │   ├── prisma.ts          # Database plugin
│           │   └── swagger.ts         # Swagger UI plugin
│           ├── utils/
│           │   └── logger.ts          # Logging utility
│           └── server.ts              # Main server file
├── prisma/
│   └── schema.prisma                  # Database schema
├── test-api.sh                        # Automated test suite
└── package.json

Route Files

Each database table gets its own route file with full CRUD operations.

Example: routes/products.ts

import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { z } from "zod";
import { prisma } from "../plugins/prisma";

// Zod validation schema
const productsSchema = z.object({
  name: z.string().min(1, "Name is required"),
  description: z.string().optional(),
  price: z.number().positive("Price must be positive"),
  stock: z.number().int().nonnegative("Stock cannot be negative").optional(),
  category: z.string().max(100).optional(),
  is_active: z.boolean().optional(),
});

export default async function productsRoutes(fastify: FastifyInstance) {
  
  // GET /products - List all products
  fastify.get('/products', async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      const items = await prisma.products.findMany();
      return reply.send(items);
    } catch (error: any) {
      request.log.error(error);
      return reply.status(500).send({ 
        error: 'Internal server error',
        message: error.message 
      });
    }
  });

  // GET /products/:id - Get single product
  fastify.get<{ Params: { id: string } }>(
    '/products/:id',
    async (request, reply) => {
      try {
        const id = parseInt(request.params.id);
        
        if (isNaN(id)) {
          return reply.status(400).send({
            error: 'Invalid ID format',
            message: 'ID must be a number'
          });
        }

        const item = await prisma.products.findUnique({ where: { id } });
        
        if (!item) {
          return reply.status(404).send({ 
            error: 'products not found' 
          });
        }
        
        return reply.send(item);
      } catch (error: any) {
        request.log.error(error);
        return reply.status(500).send({ 
          error: 'Internal server error',
          message: error.message 
        });
      }
    }
  );

  // POST /products - Create new product
  fastify.post('/products', async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      // Validate request body
      const validatedData = productsSchema.parse(request.body);
      
      const item = await prisma.products.create({
        data: validatedData as any,
      });
      
      return reply.status(201).send(item);
    } catch (error: any) {
      if (error.name === 'ZodError') {
        return reply.status(400).send({
          error: 'Validation failed',
          issues: error.errors
        });
      }
      
      request.log.error(error);
      return reply.status(500).send({ 
        error: 'Internal server error',
        message: error.message 
      });
    }
  });

  // PATCH /products/:id - Update product
  fastify.patch<{ Params: { id: string } }>(
    '/products/:id',
    async (request, reply) => {
      try {
        const id = parseInt(request.params.id);
        
        if (isNaN(id)) {
          return reply.status(400).send({
            error: 'Invalid ID format',
            message: 'ID must be a number'
          });
        }

        // Validate request body (partial update)
        const validatedData = productsSchema.partial().parse(request.body);
        
        const item = await prisma.products.update({
          where: { id },
          data: validatedData as any,
        });
        
        return reply.send(item);
      } catch (error: any) {
        if (error.name === 'ZodError') {
          return reply.status(400).send({
            error: 'Validation failed',
            issues: error.errors
          });
        }
        
        if (error.code === 'P2025') {
          return reply.status(404).send({ 
            error: 'products not found' 
          });
        }
        
        request.log.error(error);
        return reply.status(500).send({ 
          error: 'Internal server error',
          message: error.message 
        });
      }
    }
  );

  // DELETE /products/:id - Delete product
  fastify.delete<{ Params: { id: string } }>(
    '/products/:id',
    async (request, reply) => {
      try {
        const id = parseInt(request.params.id);
        
        if (isNaN(id)) {
          return reply.status(400).send({
            error: 'Invalid ID format',
            message: 'ID must be a number'
          });
        }

        await prisma.products.delete({ where: { id } });
        return reply.status(204).send();
      } catch (error: any) {
        if (error.code === 'P2025') {
          return reply.status(404).send({ 
            error: 'products not found' 
          });
        }
        
        request.log.error(error);
        return reply.status(500).send({ 
          error: 'Internal server error',
          message: error.message 
        });
      }
    }
  );
}

What’s Happening Here?

1. Zod Validation Schema:

const productsSchema = z.object({
  name: z.string().min(1, "Name is required"),
  price: z.number().positive("Price must be positive"),
  // ...
});
  • Defines validation rules for your data
  • Auto-generated based on Prisma schema types
  • Provides clear error messages

2. Error Handling:

catch (error: any) {
  if (error.name === 'ZodError') {
    return reply.status(400).send({ /* validation error */ });
  }
  if (error.code === 'P2025') {
    return reply.status(404).send({ /* not found */ });
  }
  return reply.status(500).send({ /* server error */ });
}
  • 400: Validation errors (bad input)
  • 404: Resource not found
  • 500: Server/database errors

3. Type Safety:

fastify.get<{ Params: { id: string } }>('/products/:id', ...)
  • TypeScript types ensure compile-time safety
  • Request params, body, query are all typed

Prisma Plugin

The Prisma plugin provides database access to all routes.

plugins/prisma.ts

import { FastifyPluginAsync } from 'fastify';
import { PrismaClient } from '@prisma/client';
import fp from 'fastify-plugin';

const prismaClient = new PrismaClient({
  log: ['query', 'error', 'warn'],
});

declare module 'fastify' {
  interface FastifyInstance {
    prisma: PrismaClient;
  }
}

const prismaPlugin: FastifyPluginAsync = async (fastify) => {
  fastify.decorate('prisma', prismaClient);

  fastify.addHook('onClose', async (instance) => {
    await instance.prisma.$disconnect();
  });
};

export default fp(prismaPlugin);
export const prisma = prismaClient;

Key Features:

  1. Singleton Pattern: One Prisma client for entire app
  2. Logging: Logs queries, errors, and warnings
  3. Graceful Shutdown: Disconnects on server close
  4. Global Export: prisma can be imported anywhere

Server Configuration

The main server file ties everything together.

server.ts

import Fastify from 'fastify';
import cors from '@fastify/cors';
import prismaPlugin from './plugins/prisma';
import usersRoutes from './routes/users';
import productsRoutes from './routes/products';
import ordersRoutes from './routes/orders';

const host = process.env.HOST || '0.0.0.0';
let port = parseInt(process.env.API_PORT || '3000', 10);

async function start() {
  const fastify = Fastify({
    logger: {
      transport: {
        target: 'pino-pretty',
        options: {
          translateTime: 'HH:MM:ss Z',
          ignore: 'pid,hostname',
        },
      },
    },
  });

  // Register plugins
  await fastify.register(cors, { origin: '*' });
  await fastify.register(prismaPlugin);

  // Register routes
  await fastify.register(usersRoutes);
  await fastify.register(productsRoutes);
  await fastify.register(ordersRoutes);

  // Health check
  fastify.get('/health', async () => {
    const dbStatus = await fastify.prisma.$queryRaw`SELECT 1`;
    return { 
      status: 'ok', 
      database: dbStatus ? 'connected' : 'disconnected',
      timestamp: new Date().toISOString() 
    };
  });

  // Metrics
  fastify.get('/metrics', async () => {
    const memory = process.memoryUsage();
    return {
      uptime: process.uptime(),
      memory: {
        rss: `${Math.round(memory.rss / 1024 / 1024)} MB`,
        heapUsed: `${Math.round(memory.heapUsed / 1024 / 1024)} MB`,
      },
      timestamp: new Date().toISOString(),
    };
  });

  // Auto port selection (tries 3000-3004)
  const maxPort = port + 4;
  while (port <= maxPort) {
    try {
      await fastify.listen({ port, host });
      console.log(`✅ Server running on http://${host}:${port}`);
      break;
    } catch (error: any) {
      if (error.code === 'EADDRINUSE' && port < maxPort) {
        console.log(`Port ${port} in use, trying ${port + 1}...`);
        port++;
        continue;
      }
      throw error;
    }
  }

  // Graceful shutdown
  const signals = ['SIGINT', 'SIGTERM'];
  signals.forEach((signal) => {
    process.on(signal, async () => {
      console.log(`\n${signal} received, closing server...`);
      await fastify.close();
      process.exit(0);
    });
  });
}

start().catch((err) => {
  console.error('Failed to start server:', err);
  process.exit(1);
});

Key Features:

  1. Auto Port Selection: Tries ports 3000-3004 automatically
  2. Structured Logging: Pino with pretty formatting
  3. CORS Enabled: Allows frontend access
  4. Health Check: /health endpoint with DB status
  5. Metrics: /metrics endpoint for monitoring
  6. Graceful Shutdown: Clean database disconnect

Validation with Zod

How It Works

DataBridge generates Zod schemas based on your Prisma types:

Prisma Schema:

model products {
  id          Int       @id @default(autoincrement())
  name        String    @db.VarChar(255)
  price       Decimal   @db.Decimal(10, 2)
  stock       Int?      @default(0)
  is_active   Boolean?  @default(true)
}

Generated Zod Schema:

const productsSchema = z.object({
  name: z.string().min(1).max(255),
  price: z.number().positive(),
  stock: z.number().int().nonnegative().optional(),
  is_active: z.boolean().optional(),
});

Validation Error Example

Request:

curl -X POST http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -d '{"name": "", "price": -10}'

Response:

{
  "error": "Validation failed",
  "issues": [
    {
      "path": ["name"],
      "message": "Name is required"
    },
    {
      "path": ["price"],
      "message": "Price must be positive"
    }
  ]
}

Test Script

DataBridge generates a comprehensive test suite.

test-api.sh (excerpt)

#!/bin/bash

BASE_URL="http://localhost:3000"

echo "========================================="
echo "DataBridge API Test Suite"
echo "========================================="

# Test: Health Check
echo "Test: Health Check"
RESPONSE=$(curl -s ${BASE_URL}/health)
echo $RESPONSE
if echo "$RESPONSE" | grep -q "ok"; then
  echo "✅ Pass"
else
  echo "❌ Fail"
fi

# Test: Create Product
echo "Test: POST /products (Create valid)"
RESPONSE=$(curl -s -X POST ${BASE_URL}/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Test Product", "price": 99.99}')

if echo "$RESPONSE" | grep -q "id"; then
  echo "✅ Pass"
else
  echo "❌ Fail"
fi

# Test: Validation Error
echo "Test: POST /products (Negative price)"
RESPONSE=$(curl -s -X POST ${BASE_URL}/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Test", "price": -10}')

if echo "$RESPONSE" | grep -q "Validation failed"; then
  echo "✅ Pass - Validation caught negative price"
else
  echo "❌ Fail"
fi

HTTP Status Codes

DataBridge uses standard HTTP status codes:

CodeMeaningWhen Used
200OKSuccessful GET, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestValidation error, invalid ID format
404Not FoundResource doesn’t exist
409ConflictUnique constraint violation
500Server ErrorDatabase error, unexpected error

Customizing Generated Code

Adding Custom Routes

You can add custom business logic alongside generated routes:

// routes/products-custom.ts
export default async function customProductsRoutes(fastify: FastifyInstance) {
  
  // Custom route: Search products
  fastify.get('/products/search', async (request, reply) => {
    const { q } = request.query as { q: string };
    
    const results = await fastify.prisma.products.findMany({
      where: {
        OR: [
          { name: { contains: q } },
          { description: { contains: q } },
        ],
      },
    });
    
    return results;
  });

  // Custom route: Low stock products
  fastify.get('/products/low-stock', async (request, reply) => {
    const results = await fastify.prisma.products.findMany({
      where: {
        stock: { lt: 10 },
      },
      orderBy: {
        stock: 'asc',
      },
    });
    
    return results;
  });
}

Register in server.ts:

import customProductsRoutes from './routes/products-custom';
await fastify.register(customProductsRoutes);

Extending Validation

Add custom validation rules:

const productsSchema = z.object({
  name: z.string().min(1).max(255),
  price: z.number().positive(),
  stock: z.number().int().nonnegative().optional(),
  category: z.enum(['electronics', 'clothing', 'food']), // Custom enum
  sku: z.string().regex(/^[A-Z]{3}-\d{6}$/), // Custom pattern
});

Best Practices

DO ✅

  • Keep generated files unmodified when possible
  • Add custom logic in separate files
  • Use environment variables for configuration
  • Test all endpoints with test-api.sh
  • Review Zod schemas for your business rules

DON’T ❌

  • Modify _generated.ts (it’s regenerated)
  • Hard-code database URLs in code
  • Skip validation on POST/PATCH routes
  • Ignore TypeScript errors
  • Commit .env files

Regenerating Code

If you modify your database schema:

# 1. Update database
mysql -h localhost -u root -ppassword mydb < updates.sql

# 2. Re-introspect
databridge introspect

# 3. Regenerate code
databridge generate

Note: DataBridge will overwrite generated files. Save any custom changes to separate files first!


Additional Resources

Was this page helpful?