mdashikjs/blog
All posts
My Testing Strategy for Backend APIs: What to Test and What to Skip
Backend

My Testing Strategy for Backend APIs: What to Test and What to Skip

Backend5 min

My Testing Strategy for Backend APIs: What to Test and What to Skip

100 percent code coverage is a vanity metric. The goal is confidence that the system works, not that every line has been executed. Here is how I decide what to test and at which level.

TestingNode.jsBest PracticesAPI
Share:

The testing pyramid, adjusted for APIs

  • Unit tests (many): Pure business logic, validators, formatters, calculations
  • Integration tests (some): Database queries, service interactions, API endpoint behavior
  • E2E tests (few): Critical user flows only

What I always unit test

  1. Business rules: Discount calculations, permission checks, status transitions. These are the core of your application and change frequently.
  2. Validators: Input validation logic. Every edge case — empty strings, negative numbers, SQL injection payloads.
  3. Transformers: Functions that convert between shapes (API response to domain model, domain model to database row).
describe('calculateDiscount', () => {
  it('applies 10% for orders over $100', () => {
    expect(calculateDiscount(150)).toBe(15);
  });

  it('returns 0 for orders under $100', () => {
    expect(calculateDiscount(50)).toBe(0);
  });

  it('handles exactly $100', () => {
    expect(calculateDiscount(100)).toBe(10);
  });
});

What I integration test

  1. API endpoints: Full request/response cycle. Does POST /users actually create a user and return 201?
  2. Database queries: Complex queries, especially with joins, aggregations, or pagination. Test against a real database, not mocks.
  3. Authentication flows: Login, token refresh, permission checks.
describe('POST /api/users', () => {
  it('creates a user and returns 201', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ email: 'test@example.com', name: 'Test' });

    expect(res.status).toBe(201);
    expect(res.body.data.email).toBe('test@example.com');
  });

  it('returns 422 for invalid email', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ email: 'not-an-email', name: 'Test' });

    expect(res.status).toBe(422);
    expect(res.body.code).toBe('VALIDATION_ERROR');
  });
});

What I skip

  1. Framework behavior: Do not test that NestJS decorators work. Do not test that Express routing works. The framework authors already tested that.
  2. Simple CRUD: If a service method is just this.db.findMany() with no logic, the integration test covers it.
  3. Private methods: They are tested through the public API. If a private method is complex enough to test directly, it should probably be a separate function.

The test database

Every integration test suite gets a clean database. I use a setup script that runs migrations and seeds minimal data. Tests run in transactions that roll back after each test — fast and isolated.

When to add E2E tests

Only for flows where a bug means lost revenue or broken trust: signup, checkout, payment processing. These tests are slow and brittle — keep them focused on the critical path.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.