Testing Your API

Comprehensive guide to testing DataBridge APIs with automated tests and best practices

Testing Your API

Learn how to effectively test your DataBridge-generated API.

Overview

DataBridge automatically generates test scripts, but you should also write custom tests for:

  • Business logic
  • Edge cases
  • Integration scenarios
  • Performance

Auto-Generated Tests

When you run databridge generate, you get test-api.sh with tests for:

  • Health checks
  • CRUD operations
  • Validation rules
  • Error handling

Running Generated Tests

# Start your API
npm run dev

# In another terminal
chmod +x test-api.sh
./test-api.sh

Example Output

=========================================
DataBridge API Test Suite
=========================================

Test: Health Check
✅ Pass

Test: GET /products (List all)
✅ Pass

Test: POST /products (Create valid)
✅ Pass

Test: POST /products (Missing required field)
✅ Pass - Validation caught missing field

Test: POST /products (Negative price)
✅ Pass - Validation caught negative price

=========================================
Test Suite Complete: 5/5 passed
=========================================

Manual Testing with cURL

GET Requests

# List all products
curl http://localhost:3000/products

# Get single product
curl http://localhost:3000/products/1

# With query parameters
curl "http://localhost:3000/products?category=electronics&sort=price"

POST Requests

# Create product
curl -X POST http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Laptop Pro",
    "description": "High-performance laptop",
    "price": 1299.99,
    "stock": 10,
    "category": "electronics"
  }'

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

PATCH Requests

# Update product
curl -X PATCH http://localhost:3000/products/1 \
  -H "Content-Type: application/json" \
  -d '{"stock": 15, "price": 1199.99}'

DELETE Requests

# Delete product
curl -X DELETE http://localhost:3000/products/1 \
  -w "\nHTTP Status: %{http_code}\n"

Unit Testing with Vitest

Setup

npm install -D vitest @vitest/ui

vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Testing Validation Schemas

tests/validation.test.ts:

import { describe, it, expect } from 'vitest';
import { z } from 'zod';

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

describe('Product Validation', () => {
  it('accepts valid product', () => {
    const valid = {
      name: 'Laptop',
      price: 999.99,
      stock: 10,
    };
    
    const result = productSchema.safeParse(valid);
    expect(result.success).toBe(true);
  });

  it('rejects empty name', () => {
    const invalid = {
      name: '',
      price: 999.99,
    };
    
    const result = productSchema.safeParse(invalid);
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.errors[0].path).toEqual(['name']);
    }
  });

  it('rejects negative price', () => {
    const invalid = {
      name: 'Laptop',
      price: -10,
    };
    
    const result = productSchema.safeParse(invalid);
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.errors[0].path).toEqual(['price']);
    }
  });

  it('rejects string for stock', () => {
    const invalid = {
      name: 'Laptop',
      price: 999.99,
      stock: 'ten' as any,
    };
    
    const result = productSchema.safeParse(invalid);
    expect(result.success).toBe(false);
  });

  it('accepts optional stock', () => {
    const valid = {
      name: 'Laptop',
      price: 999.99,
    };
    
    const result = productSchema.safeParse(valid);
    expect(result.success).toBe(true);
  });
});

Run tests:

npx vitest
npx vitest --ui  # Open UI
npx vitest --coverage  # Generate coverage

Integration Testing

Test entire API routes with real HTTP requests.

Setup

npm install -D supertest @types/supertest

Testing Routes

tests/products.integration.test.ts:

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import productsRoutes from '../src/routes/products';
import prismaPlugin from '../src/plugins/prisma';

describe('Products API', () => {
  let fastify: FastifyInstance;

  beforeAll(async () => {
    fastify = Fastify();
    await fastify.register(prismaPlugin);
    await fastify.register(productsRoutes);
    await fastify.ready();
  });

  afterAll(async () => {
    await fastify.close();
  });

  describe('GET /products', () => {
    it('returns array of products', async () => {
      const response = await fastify.inject({
        method: 'GET',
        url: '/products',
      });

      expect(response.statusCode).toBe(200);
      expect(Array.isArray(JSON.parse(response.body))).toBe(true);
    });
  });

  describe('POST /products', () => {
    it('creates product with valid data', async () => {
      const response = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {
          name: 'Test Product',
          price: 99.99,
          stock: 10,
        },
      });

      expect(response.statusCode).toBe(201);
      const product = JSON.parse(response.body);
      expect(product).toHaveProperty('id');
      expect(product.name).toBe('Test Product');
      expect(product.price).toBe('99.99');
    });

    it('rejects invalid data', async () => {
      const response = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {
          name: '',
          price: -10,
        },
      });

      expect(response.statusCode).toBe(400);
      const error = JSON.parse(response.body);
      expect(error.error).toBe('Validation failed');
      expect(error.issues).toBeDefined();
    });

    it('rejects missing required fields', async () => {
      const response = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {},
      });

      expect(response.statusCode).toBe(400);
    });
  });

  describe('GET /products/:id', () => {
    it('returns product by id', async () => {
      // First create a product
      const createResponse = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {
          name: 'Find Me',
          price: 49.99,
        },
      });
      const created = JSON.parse(createResponse.body);

      // Then fetch it
      const response = await fastify.inject({
        method: 'GET',
        url: `/products/${created.id}`,
      });

      expect(response.statusCode).toBe(200);
      const product = JSON.parse(response.body);
      expect(product.id).toBe(created.id);
      expect(product.name).toBe('Find Me');
    });

    it('returns 404 for non-existent product', async () => {
      const response = await fastify.inject({
        method: 'GET',
        url: '/products/99999',
      });

      expect(response.statusCode).toBe(404);
    });

    it('returns 400 for invalid id', async () => {
      const response = await fastify.inject({
        method: 'GET',
        url: '/products/abc',
      });

      expect(response.statusCode).toBe(400);
    });
  });

  describe('PATCH /products/:id', () => {
    it('updates product', async () => {
      // Create product
      const createResponse = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {
          name: 'Original Name',
          price: 100,
        },
      });
      const created = JSON.parse(createResponse.body);

      // Update it
      const response = await fastify.inject({
        method: 'PATCH',
        url: `/products/${created.id}`,
        payload: {
          name: 'Updated Name',
        },
      });

      expect(response.statusCode).toBe(200);
      const updated = JSON.parse(response.body);
      expect(updated.name).toBe('Updated Name');
      expect(updated.price).toBe(created.price); // Unchanged
    });
  });

  describe('DELETE /products/:id', () => {
    it('deletes product', async () => {
      // Create product
      const createResponse = await fastify.inject({
        method: 'POST',
        url: '/products',
        payload: {
          name: 'Delete Me',
          price: 10,
        },
      });
      const created = JSON.parse(createResponse.body);

      // Delete it
      const response = await fastify.inject({
        method: 'DELETE',
        url: `/products/${created.id}`,
      });

      expect(response.statusCode).toBe(204);

      // Verify it's gone
      const getResponse = await fastify.inject({
        method: 'GET',
        url: `/products/${created.id}`,
      });
      expect(getResponse.statusCode).toBe(404);
    });
  });
});

E2E Testing with Playwright

Test your API through the browser (for full stack apps).

Setup

npm install -D @playwright/test
npx playwright install

playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  use: {
    baseURL: 'http://localhost:3000',
  },
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

E2E Test Example

tests/e2e/products.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Products API E2E', () => {
  test('can create and fetch product', async ({ request }) => {
    // Create product
    const createResponse = await request.post('/products', {
      data: {
        name: 'E2E Test Product',
        price: 149.99,
        stock: 25,
      },
    });
    expect(createResponse.ok()).toBeTruthy();
    
    const created = await createResponse.json();
    expect(created).toHaveProperty('id');

    // Fetch product
    const getResponse = await request.get(`/products/${created.id}`);
    expect(getResponse.ok()).toBeTruthy();
    
    const fetched = await getResponse.json();
    expect(fetched.name).toBe('E2E Test Product');
  });

  test('validation errors return 400', async ({ request }) => {
    const response = await request.post('/products', {
      data: {
        name: '',
        price: -10,
      },
    });
    
    expect(response.status()).toBe(400);
    const error = await response.json();
    expect(error.error).toBe('Validation failed');
  });
});

Run tests:

npx playwright test
npx playwright test --ui
npx playwright show-report

Load Testing

Test API performance under load.

Using Apache Bench (ab)

# 1000 requests, 10 concurrent
ab -n 1000 -c 10 http://localhost:3000/products

Using autocannon

npm install -g autocannon

# Run load test
autocannon -c 100 -d 30 http://localhost:3000/products

# Output:
# ┌─────────┬────────┬─────────┬─────────┬─────────┬─────────┐
# │ Stat    │ 2.5%   │ 50%     │ 97.5%   │ 99%     │ Avg     │
# ├─────────┼────────┼─────────┼─────────┼─────────┼─────────┤
# │ Latency │ 10 ms  │ 15 ms   │ 45 ms   │ 60 ms   │ 18 ms   │
# └─────────┴────────┴─────────┴─────────┴─────────┴─────────┘
# 100k requests in 30s, 15 MB read

Custom Load Test Script

tests/load-test.ts:

import autocannon from 'autocannon';

async function runLoadTest() {
  const result = await autocannon({
    url: 'http://localhost:3000',
    connections: 100,
    duration: 30,
    requests: [
      {
        method: 'GET',
        path: '/products',
      },
      {
        method: 'GET',
        path: '/health',
      },
    ],
  });

  console.log(result);
}

runLoadTest();

Database Testing

Test Database Setup

Use separate test database:

.env.test:

DATABASE_URL="mysql://root:password@localhost:3306/mydb_test"

Seeding Test Data

prisma/seed.ts:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // Clear existing data
  await prisma.products.deleteMany();
  await prisma.users.deleteMany();

  // Seed test data
  await prisma.users.create({
    data: {
      email: 'test@example.com',
      name: 'Test User',
    },
  });

  await prisma.products.createMany({
    data: [
      {
        name: 'Test Product 1',
        price: 99.99,
        stock: 10,
      },
      {
        name: 'Test Product 2',
        price: 149.99,
        stock: 5,
      },
    ],
  });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Run seed:

npx prisma db seed

CI/CD Integration

GitHub Actions

.github/workflows/test.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: test
          MYSQL_DATABASE: mydb_test
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Generate Prisma client
        run: npx prisma generate
      
      - name: Run migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: mysql://root:test@127.0.0.1:3306/mydb_test
      
      - name: Run tests
        run: npm test
        env:
          DATABASE_URL: mysql://root:test@127.0.0.1:3306/mydb_test
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json

Best Practices

DO ✅

  • Write tests before fixing bugs (TDD)
  • Test edge cases and error conditions
  • Use separate test database
  • Clean up test data after tests
  • Mock external services
  • Test validation thoroughly
  • Test authentication/authorization
  • Monitor test performance
  • Run tests in CI/CD
  • Maintain > 80% coverage

DON’T ❌

  • Test against production database
  • Skip error case testing
  • Hard-code test data IDs
  • Leave test data in database
  • Test only happy paths
  • Ignore flaky tests
  • Skip integration tests
  • Test implementation details

Test Coverage

Check coverage:

npx vitest --coverage

Example output:

 % Coverage report from v8
----------------------------|---------|----------|---------|---------|
File                        | % Stmts | % Branch | % Funcs | % Lines |
----------------------------|---------|----------|---------|---------|
All files                   |   92.5  |   87.3   |   95.0  |   92.8  |
 routes/products.ts         |   95.2  |   90.0   |   100   |   95.4  |
 routes/users.ts            |   93.1  |   88.0   |   100   |   93.5  |
 plugins/prisma.ts          |   85.0  |   75.0   |   80.0  |   85.2  |
----------------------------|---------|----------|---------|---------|

Additional Resources

Was this page helpful?