Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/grafana/k6-docs/llms.txt

Use this file to discover all available pages before exploring further.

Test REST API CRUD operations (Create, Read, Update, Delete) with complete request flows and authentication.

Understanding CRUD

CRUD operations map to HTTP methods:
  • Create: POST - Create a new resource
  • Read: GET - Retrieve a resource
  • Update: PUT or PATCH - Modify an existing resource
  • Delete: DELETE - Remove a resource

Test Workflow

This example tests the QuickPizza API with a complete CRUD flow:
  1. Setup: Create a user and retrieve an authentication token
  2. Create: Post a new pizza rating
  3. Read: Fetch the list of ratings
  4. Update: Modify the rating (change stars)
  5. Delete: Remove the rating

Core k6 API Example

Using standard k6 HTTP and check APIs:
import http from 'k6/http';
import { check, group } from 'k6';

export const options = {
  vus: 1,
  iterations: 1,
};

// Create a random string of given length
function randomString(length, charset = '') {
  if (!charset) charset = 'abcdefghijklmnopqrstuvwxyz';
  let res = '';
  while (length--) res += charset[(Math.random() * charset.length) | 0];
  return res;
}

const USERNAME = `${randomString(10)}@example.com`;
const PASSWORD = 'secret';
const BASE_URL = 'https://quickpizza.grafana.com';

// Register a new user and retrieve authentication token
export function setup() {
  const res = http.post(
    `${BASE_URL}/api/users`,
    JSON.stringify({
      username: USERNAME,
      password: PASSWORD,
    })
  );

  check(res, { 'created user': (r) => r.status === 201 });

  const loginRes = http.post(
    `${BASE_URL}/api/users/token/login`,
    JSON.stringify({
      username: USERNAME,
      password: PASSWORD,
    })
  );

  const authToken = loginRes.json('token');
  check(authToken, { 'logged in successfully': () => authToken.length > 0 });

  return authToken;
}

export default function (authToken) {
  // Set authorization header for all requests
  const requestConfigWithTag = (tag) => ({
    headers: {
      Authorization: `Bearer ${authToken}`,
    },
    tags: Object.assign(
      {},
      {
        name: 'PrivateRatings',
      },
      tag
    ),
  });

  let URL = `${BASE_URL}/api/ratings`;

  group('01. Create a new rating', () => {
    const payload = {
      stars: 2,
      pizza_id: 1, // Pizza ID 1 already exists in the database
    };

    const res = http.post(URL, JSON.stringify(payload), requestConfigWithTag({ name: 'Create' }));

    if (check(res, { 'Rating created correctly': (r) => r.status === 201 })) {
      URL = `${URL}/${res.json('id')}`;
    } else {
      console.log(`Unable to create rating ${res.status} ${res.body}`);
      return;
    }
  });

  group('02. Fetch my ratings', () => {
    const res = http.get(`${BASE_URL}/api/ratings`, requestConfigWithTag({ name: 'Fetch' }));
    check(res, { 'retrieve ratings status': (r) => r.status === 200 });
    check(res.json(), { 'retrieved ratings list': (r) => r.ratings.length > 0 });
  });

  group('03. Update the rating', () => {
    const payload = { stars: 5 };
    const res = http.put(URL, JSON.stringify(payload), requestConfigWithTag({ name: 'Update' }));
    const isSuccessfulUpdate = check(res, {
      'Update worked': () => res.status === 200,
      'Updated stars number is correct': () => res.json('stars') === 5,
    });

    if (!isSuccessfulUpdate) {
      console.log(`Unable to update the rating ${res.status} ${res.body}`);
      return;
    }
  });

  group('04. Delete the rating', () => {
    const delRes = http.del(URL, null, requestConfigWithTag({ name: 'Delete' }));

    const isSuccessfulDelete = check(null, {
      'Rating was deleted correctly': () => delRes.status === 204,
    });

    if (!isSuccessfulDelete) {
      console.log('Rating was not deleted properly');
      return;
    }
  });
}

Modern API Example (httpx + k6chaijs)

Using the newer httpx and k6chaijs libraries for a more expressive syntax:
import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js';
import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js';
import {
  randomIntBetween,
  randomString,
} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

export const options = {
  vus: 1,
  iterations: 1,
};

const USERNAME = `user${randomIntBetween(1, 100000)}@example.com`;
const PASSWORD = 'secret';

const session = new Httpx({ baseURL: 'https://quickpizza.grafana.com' });

// Register a new user and retrieve authentication token
export function setup() {
  let authToken = null;

  describe(`setup - create a test user ${USERNAME}`, () => {
    const resp = session.post(
      `/api/users`,
      JSON.stringify({
        username: USERNAME,
        password: PASSWORD,
      })
    );

    expect(resp.status, 'User create status').to.equal(201);
    expect(resp, 'User create valid json response').to.have.validJsonBody();
  });

  describe(`setup - Authenticate the new user ${USERNAME}`, () => {
    const resp = session.post(
      `/api/users/token/login`,
      JSON.stringify({
        username: USERNAME,
        password: PASSWORD,
      })
    );

    expect(resp.status, 'Authenticate status').to.equal(200);
    expect(resp, 'Authenticate valid json response').to.have.validJsonBody();
    authToken = resp.json('token');
    expect(authToken, 'Authentication token').to.be.a('string');
  });

  return authToken;
}

export default function (authToken) {
  // Set authorization header on the session
  session.addHeader('Authorization', `Bearer ${authToken}`);

  describe('01. Create a new rating', (t) => {
    const payload = {
      stars: 2,
      pizza_id: 1, // Pizza ID 1 already exists in the database
    };

    session.addTag('name', 'Create');
    const resp = session.post(`/api/ratings`, JSON.stringify(payload));

    expect(resp.status, 'Rating creation status').to.equal(201);
    expect(resp, 'Rating creation valid json response').to.have.validJsonBody();

    session.newRatingId = resp.json('id');
  });

  describe('02. Fetch my ratings', (t) => {
    session.clearTag('name');
    const resp = session.get('/api/ratings');

    expect(resp.status, 'Fetch ratings status').to.equal(200);
    expect(resp, 'Fetch ratings valid json response').to.have.validJsonBody();
    expect(resp.json('ratings').length, 'Number of ratings').to.be.above(0);
  });

  describe('03. Update the rating', (t) => {
    const payload = {
      stars: 5,
    };

    const resp = session.patch(`/api/ratings/${session.newRatingId}`, JSON.stringify(payload));

    expect(resp.status, 'Rating patch status').to.equal(200);
    expect(resp, 'Fetch rating valid json response').to.have.validJsonBody();
    expect(resp.json('stars'), 'Stars').to.equal(payload.stars);

    // Read rating again to verify the update worked
    const resp1 = session.get(`/api/ratings/${session.newRatingId}`);

    expect(resp1.status, 'Fetch rating status').to.equal(200);
    expect(resp1, 'Fetch rating valid json response').to.have.validJsonBody();
    expect(resp1.json('stars'), 'Stars').to.equal(payload.stars);
  });

  describe('04. Delete the rating', (t) => {
    const resp = session.delete(`/api/ratings/${session.newRatingId}`);

    expect(resp.status, 'Rating delete status').to.equal(204);
  });
}
The httpx session automatically handles the base URL and maintains headers across requests, making your test code cleaner.

Running the Test

k6 run api-crud-test.js

Key Concepts

Groups

Organize related requests and view grouped metrics:
group('User Registration', () => {
  // Multiple related requests
  const res1 = http.post('/api/register', data);
  const res2 = http.post('/api/verify', verifyData);
});

Checks vs Thresholds

Checks validate individual responses but don’t fail the test:
check(res, { 'status is 200': (r) => r.status === 200 });
Thresholds define pass/fail criteria for the entire test:
export const options = {
  thresholds: {
    'http_req_duration': ['p(95)<500'], // 95% of requests under 500ms
    'checks': ['rate>0.9'],             // 90% of checks pass
  },
};

Tags

Add custom tags to filter and analyze metrics:
const params = {
  tags: {
    name: 'CreateRating',
    api_version: 'v2',
  },
};

http.post(url, data, params);

Best Practices

Avoid creating test data in every VU iteration. Use the setup() function to create shared data once:
export function setup() {
  // Create user once, return credentials
  return { token: 'auth-token' };
}

export default function(data) {
  // Use shared token in all VU iterations
  http.get(url, { headers: { Authorization: data.token } });
}
Always validate responses before extracting data:
const res = http.post(url, data);

if (check(res, { 'created': (r) => r.status === 201 })) {
  const id = res.json('id');
  // Continue with id
} else {
  console.log(`Failed: ${res.status} ${res.body}`);
  return; // Exit early
}
Remove test data after the test completes:
export function teardown(data) {
  http.del(`https://api.example.com/users/${data.userId}`, {
    headers: { Authorization: `Bearer ${data.token}` },
  });
}

HTTP Authentication

Learn about authentication methods

Correlation & Dynamic Data

Extract and reuse response data

Test Lifecycle

Understand setup, VU, and teardown stages

Checks & Thresholds

Define pass/fail criteria

Build docs developers (and LLMs) love