> ## Documentation Index
> Fetch the complete documentation index at: https://checkly-422f444a-onboarding-guides-plan.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Structure a Checkly project for a real codebase

> Organize checks by service with groups, keep shared settings in one config, and deploy monitoring from CI on every merge to main.

export const CopyPromptButton = ({label = "Copy setup prompt", targetId = "ai-setup-prompt"}) => {
  const [copied, setCopied] = useState(false);
  const handleCopy = async () => {
    try {
      const el = document.getElementById(targetId);
      const code = el?.querySelector("code");
      const text = code?.textContent || el?.textContent || "";
      await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error("Failed to copy prompt:", err);
    }
  };
  return <button onClick={handleCopy} className="inline-flex items-center gap-2 px-5 py-3 rounded-lg font-semibold text-base
        border border-gray-200 dark:border-gray-700
        bg-white dark:bg-gray-800
        text-gray-800 dark:text-gray-200
        hover:bg-gray-50 dark:hover:bg-gray-700
        transition-colors cursor-pointer my-2">
      {copied ? <>
          <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
            <path d="M13.3 4.3L6 11.6L2.7 8.3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          Copied!
        </> : <>
          <svg width="18" height="18" viewBox="0 0 16 16" fill="none">
            <rect x="5" y="5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.5" />
            <path d="M11 5V3.5C11 2.67 10.33 2 9.5 2H3.5C2.67 2 2 2.67 2 3.5V9.5C2 10.33 2.67 11 3.5 11H5" stroke="currentColor" strokeWidth="1.5" />
          </svg>
          {label}
        </>}
    </button>;
};

By the end of this guide, your monitoring repository has one folder per service, shared defaults in a single config, a group per service with its own locations and variables, and a workflow that redeploys monitoring on every merge to `main`.

<Frame>
  <img src="https://mintcdn.com/checkly-422f444a-onboarding-guides-plan/_xcqrjDu0C_8g4Qz/images/guides/structure-a-project/outcome-groups.png?fit=max&auto=format&n=_xcqrjDu0C_8g4Qz&q=85&s=6518fb0272c3b4e593d75c8e01fb6f79" alt="The Checkly check list filtered to Shop, showing the Shop API and Shop web groups with two checks each, tagged api and web" width="2400" height="880" data-path="images/guides/structure-a-project/outcome-groups.png" />
</Frame>

To follow along without your own app, clone the [sample project](https://github.com/checkly/docs/tree/main/samples/guides/structure-a-project). It monitors the [Danube demo shop](https://danube-web.shop) and its API.

<Accordion title="Let your agent do it" icon="sparkles">
  To run this guide from your terminal or your coding agent, run `npx checkly init` in your project first. It installs the Checkly CLI and [Checkly Skills](/ai/skills) for your agent. Then paste the prompt below into Claude Code, Cursor, Codex, or any agent that supports skills. It builds the same setup as this guide, proves it with `npx checkly test --record`, and stops for your confirmation before `npx checkly deploy`.

  <div id="ai-setup-prompt">
    ```txt Prompt theme={null}
    Restructure the Checkly project in this repository so it scales with the codebase.

    Goal: one folder per service under `__checks__`, shared defaults in `checkly.config.ts`, a `CheckGroupV2` per service, and a GitHub Actions workflow that deploys on merge to `main`.

    Success criteria:
    1. Look at the services this repository contains and propose the service folders. Ask me to confirm before moving files.
    2. Move shared settings (frequency, locations, tags, alert channels) into the `checks` defaults of `checkly.config.ts`. Keep `logicalId` unchanged.
    3. Put one `group.ts` in each service folder with a `CheckGroupV2`. Give the API group an `API_BASE_URL` environment variable and make its checks use `{{API_BASE_URL}}`.
    4. Put credentials in secrets with `npx checkly env add <KEY> <value> --secret`, never in code.
    5. Add `.github/workflows/checkly-deploy.yml` that runs `npx checkly deploy --force` on pushes to `main` that touch `__checks__/**` or `checkly.config.ts`, reading `CHECKLY_API_KEY` from secrets and `CHECKLY_ACCOUNT_ID` from variables.
    6. Run `npx checkly test --record`, then show me `npx checkly deploy --preview` and wait for my confirmation before deploying.

    Explain each file you changed and why.
    ```
  </div>

  <CopyPromptButton />

  The steps below are the same work done by hand, so you can see what the agent built and why each piece is there.
</Accordion>

## Step 1: One folder per service

The quickstart scaffold puts every check in a flat `__checks__` folder. That works for four files and stops working at forty. Split it by the services in your system, so the folder tells you who owns a check and what it covers.

```text Project layout theme={null}
checkly.config.ts
__checks__/
  alert-channels.ts
  web/
    group.ts
    homepage.check.ts
    homepage.spec.ts
    uptime.check.ts
  api/
    group.ts
    books.check.ts
```

Each service folder gets a `group.ts` and its checks. The config's `checkMatch` glob finds check files at any depth, so nothing else changes when you add a folder.

## Step 2: Shared settings in one config

Put the defaults every check should inherit in `checkly.config.ts`, and the alert channel they share next to the checks.

```ts checkly.config.ts highlight={10-16} theme={null}
import { defineConfig } from 'checkly'
import { Frequency } from 'checkly/constructs'
import { opsEmail } from './__checks__/alert-channels'

export default defineConfig({
  projectName: 'Docs guide: Structure a project',
  logicalId: 'docs-guide-structure-a-project',
  repoUrl: 'https://github.com/checkly/docs',
  checks: {
    // Defaults every check inherits unless a group or the check overrides them.
    frequency: Frequency.EVERY_10M,
    locations: ['us-east-1', 'eu-west-1'],
    tags: ['shop'],
    alertChannels: [opsEmail],
    // Where to find checks. One folder per service keeps ownership obvious.
    checkMatch: '**/__checks__/**/*.check.ts',
  },
  cli: {
    runLocation: 'eu-west-1',
  },
})
```

```ts __checks__/alert-channels.ts theme={null}
import { EmailAlertChannel } from 'checkly/constructs'

// One channel, attached to every check through the project defaults.
// The alerting guide tunes when and how often it fires.
export const opsEmail = new EmailAlertChannel('ops-email', {
  address: 'ops@example.com',
  sendFailure: true,
  sendRecovery: true,
  sendDegraded: false,
})
```

Settings resolve from the most specific level: a value on the check wins over the group, which wins over the project. Keep `logicalId` stable. Changing it makes Checkly treat the project as new and drops its history.

## Step 3: A group per service

A group holds the settings that are true for a whole service and lets you run or mute that service as a unit. The storefront runs from three regions so a regional CDN problem shows up as a single-location failure.

```ts __checks__/web/group.ts theme={null}
import { CheckGroupV2 } from 'checkly/constructs'

// The storefront: user-facing pages. Runs from three regions so a
// regional CDN problem shows up as a single-location failure.
export const webGroup = new CheckGroupV2('shop-web', {
  name: 'Shop web',
  tags: ['web'],
  locations: ['us-east-1', 'eu-west-1', 'ap-southeast-1'],
  runParallel: true,
})
```

A check joins the group through its `group` property. The uptime monitor also overrides the project's ten-minute default, because a URL monitor is cheap enough to run every minute.

```ts __checks__/web/uptime.check.ts highlight={6,8} theme={null}
import { Frequency, UrlAssertionBuilder, UrlMonitor } from 'checkly/constructs'
import { webGroup } from './group'

new UrlMonitor('shop-homepage-uptime', {
  name: 'Homepage uptime',
  group: webGroup,
  // Overrides the project default: uptime is cheap, run it more often.
  frequency: Frequency.EVERY_1M,
  degradedResponseTime: 3000,
  maxResponseTime: 10000,
  request: {
    url: 'https://danube-web.shop/',
    followRedirects: true,
    assertions: [UrlAssertionBuilder.statusCode().equals(200)],
  },
})
```

The API group carries a variable instead of locations. Every API check in the folder reads `{{API_BASE_URL}}`, so moving the API to a new host is a one-line change.

```ts __checks__/api/group.ts highlight={8-10} theme={null}
import { CheckGroupV2 } from 'checkly/constructs'

// The backend API. The base URL lives on the group, so every API check
// in this folder reads {{API_BASE_URL}} instead of repeating the host.
export const apiGroup = new CheckGroupV2('shop-api', {
  name: 'Shop API',
  tags: ['api'],
  environmentVariables: [
    { key: 'API_BASE_URL', value: 'https://danube-web.shop/api' },
  ],
})
```

```ts __checks__/api/books.check.ts highlight={11} theme={null}
import { ApiCheck, AssertionBuilder } from 'checkly/constructs'
import { apiGroup } from './group'

new ApiCheck('shop-api-books', {
  name: 'Books catalog',
  group: apiGroup,
  degradedResponseTime: 2000,
  maxResponseTime: 5000,
  request: {
    method: 'GET',
    url: '{{API_BASE_URL}}/books',
    assertions: [
      AssertionBuilder.statusCode().equals(200),
      AssertionBuilder.headers('content-type').contains('application/json'),
      AssertionBuilder.jsonBody('$.length').greaterThan(0),
      AssertionBuilder.jsonBody('$[0].title').notEmpty(),
    ],
  },
})
```

<Tip>
  Credentials go in secrets, not in code. Add one globally with `npx checkly env add API_TOKEN "..." --secret` and read it the same way, as `{{API_TOKEN}}` in API checks or `process.env.API_TOKEN` in scripts. See [environment variables and secrets](/platform/variables).
</Tip>

Run the project on Checkly's infrastructure to confirm every check parses and passes.

```bash Terminal theme={null}
npx checkly test
```

```text Terminal theme={null}
Parsing your project... ✅
Validating project resources... ✅
Bundling project resources... ✅
Uploading Playwright tests... ✅

Running 3 checks in eu-west-1.

__checks__/api/books.check.ts
  ✔ Books catalog (217ms)
__checks__/web/homepage.check.ts
  ✔ Homepage renders (5s)
__checks__/web/uptime.check.ts
  ✔ Homepage uptime (216ms)

3 passed, 3 total
```

After you deploy in the next step, each group gets its own page with the checks it owns, their combined availability, and a button to run them all at once.

<Frame>
  <img src="https://mintcdn.com/checkly-422f444a-onboarding-guides-plan/_xcqrjDu0C_8g4Qz/images/guides/structure-a-project/group-web.png?fit=max&auto=format&n=_xcqrjDu0C_8g4Qz&q=85&s=16b5832f6745646c5d92bdc70de186da" alt="The Shop web group page in Checkly showing two passing checks, availability and response time stats, and recent run results from three locations" width="2400" height="1200" data-path="images/guides/structure-a-project/group-web.png" />
</Frame>

## Step 4: Deploy from CI on every merge

Preview what the first deploy creates, then deploy once from your machine.

```bash Terminal theme={null}
npx checkly deploy --preview
```

```text Terminal theme={null}
Create:
    EmailAlertChannel: ops-email
    ApiCheck: shop-api-books
    BrowserCheck: shop-homepage
    UrlMonitor: shop-homepage-uptime
    CheckGroupV2: shop-api
    CheckGroupV2: shop-web
```

```bash Terminal theme={null}
npx checkly deploy
```

From here on, let CI do it. The workflow below redeploys the project whenever a merge to `main` touches monitoring code. Checkly diffs the project against what is deployed and applies only the change, so re-running it is safe.

```yaml .github/workflows/checkly-deploy.yml theme={null}
name: Deploy Checkly monitoring

# Every merge to main that touches monitoring code redeploys the project.
# Checkly diffs the project against what is deployed and applies the change.
on:
  push:
    branches: [main]
    paths:
      - '__checks__/**'
      - 'checkly.config.ts'
      - 'package-lock.json'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx checkly deploy --force
        env:
          CHECKLY_API_KEY: ${{ secrets.CHECKLY_API_KEY }}
          CHECKLY_ACCOUNT_ID: ${{ vars.CHECKLY_ACCOUNT_ID }}
```

Store `CHECKLY_API_KEY` as a repository secret and `CHECKLY_ACCOUNT_ID` as a repository variable. Both come from your Checkly settings, described in [CLI authentication](/cli/authentication). The `--force` flag skips the interactive confirmation.

<Note>
  This workflow deploys after a merge. Running `npx checkly test` on every pull request, so a broken check never reaches `main`, is the subject of the [checks on every deploy](/guides/sdlc-monitoring) guide.
</Note>

## Verify it works

Add a check the way a teammate would: one new file in the service folder, nothing else.

```ts __checks__/api/book-detail.check.ts theme={null}
import { ApiCheck, AssertionBuilder } from 'checkly/constructs'
import { apiGroup } from './group'

new ApiCheck('shop-api-book-detail', {
  name: 'Book detail',
  group: apiGroup,
  degradedResponseTime: 2000,
  maxResponseTime: 5000,
  request: {
    method: 'GET',
    url: '{{API_BASE_URL}}/books/1',
    assertions: [
      AssertionBuilder.statusCode().equals(200),
      AssertionBuilder.jsonBody('$.title').equals('Haben oder haben'),
    ],
  },
})
```

The preview shows exactly one new resource. Everything else is untouched.

```bash Terminal theme={null}
npx checkly deploy --preview
```

```text Terminal theme={null}
Create:
    ApiCheck: shop-api-book-detail

Update and Unchanged:
    EmailAlertChannel: ops-email
    ApiCheck: shop-api-books
    BrowserCheck: shop-homepage
    UrlMonitor: shop-homepage-uptime
    CheckGroupV2: shop-api
    CheckGroupV2: shop-web
```

Commit the file and merge. The workflow deploys it, and the new check appears under Shop API with the group's variable and the project's alert channel already applied.

## Next

[Turn your Playwright tests into monitors](/guides/playwright-testing-to-monitoring): the tests you already have become the checks in your `web` folder.

## Reference

* [Project construct](/constructs/project)
* [CheckGroupV2 construct](/constructs/check-group-v2) and [Groups](/platform/groups)
* [Environment variables and secrets](/platform/variables)
* [Run Checkly from GitHub Actions](/integrations/ci-cd/github/actions) and [CLI authentication](/cli/authentication)
* [`npx checkly test`](/cli/checkly-test) and [`npx checkly deploy`](/cli/checkly-deploy)
* [Checkly Skills](/ai/skills)
