# Your First API

**Page:** getting-started/your-first-api

[Download Raw Markdown](./getting-started/your-first-api.md)

---

# Your First API

**Write a script. It's already an API.**

With hoody-exec, any script you write to its scripts directory becomes a live HTTP endpoint — instantly. No Express. No Docker. No CI/CD. You write the file, and the URL exists. Always write exec scripts through the exec service's own `scripts/write` endpoint (not the Files service) so they land in the exec-managed scripts directory and pass validation.

This is what "scripts become APIs" actually means.

---

## Step 1: Write a Script


  
    ```bash
    # Write a script to the exec scripts directory (path is relative to that dir)
    hoody exec scripts write --path "api/hello.js" --create-dirs --validate --content \
    '// @mode serverless
    const name = metadata.query.name || "World";
    return { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };'
    ```
  
  
    ```typescript
    // Write the script via the exec service (not the Files service)
    const execUrl = `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu`;

    const script = `// @mode serverless
    const name = metadata.query.name || "World";
    return { message: \`Hello, \${name}!\`, timestamp: new Date().toISOString() };`;

    await fetch(`${execUrl}/api/v1/exec/scripts/write`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        path: 'api/hello.js',
        content: script,
        createDirs: true,
        validate: true
      })
    });
    ```
  
  
    ```bash
    # Write the script via the exec service's scripts/write endpoint.
    # `path` is relative to the exec scripts directory.
    curl -X POST "https://$PROJECT-$CONTAINER-exec-1.$NODE.containers.hoody.icu/api/v1/exec/scripts/write" \
      -H "Content-Type: application/json" \
      -d '{
        "path": "api/hello.js",
        "content": "// @mode serverless\nconst name = metadata.query.name || \"World\";\nreturn { message: `Hello, ${name}!`, timestamp: new Date().toISOString() };",
        "createDirs": true,
        "validate": true
      }'
    ```
  


---

## Step 2: Call It

Your script is already live at:

```
https://{projectId}-{containerId}-exec-1.{serverName}.containers.hoody.icu/api/hello
```


  
    ```bash
    # Call your new API endpoint directly via its URL
    curl "https://$PROJECT_ID-$CONTAINER_ID-exec-1.$SERVER.containers.hoody.icu/api/hello?name=Developer"
    ```
  
  
    ```typescript
    // Exec scripts are live HTTP endpoints — call them directly
    const response = await fetch(
      `https://${PROJECT_ID}-${CONTAINER_ID}-exec-1.${SERVER}.containers.hoody.icu/api/hello?name=Developer`
    );
    const data = await response.json();
    console.log(data);
    // { message: "Hello, Developer!", timestamp: "2026-03-04T..." }
    ```
  
  
    ```bash
    curl "https://$PROJECT-$CONTAINER-exec-1.$SERVER.containers.hoody.icu/api/hello?name=Developer"
    ```

    Response:

    ```json
    { "message": "Hello, Developer!", "timestamp": "2026-03-04T12:00:00.000Z" }
    ```
  


That URL works from anywhere: a browser, a webhook, an AI agent, a phone. No deployment step.

---

## Step 3: Chain Services

The real power is composition. Your exec scripts can call any other service in the container. Build the sibling service URLs from the same project/container identifiers you already have — exec's `metadata` carries `projectId`, `containerId`, `nodeId` (the `node-…` server name), and `domain`, so compose the host explicitly:

```javascript
// @mode serverless
// File: /scripts/api/deploy-report.js

const { projectId, containerId, nodeId, domain } = metadata;
const host = (svc, idx = 1) =>
  `https://${projectId}-${containerId}-${svc}-${idx}.${nodeId}.${domain}`;

// Run the build (ephemeral=true auto-creates an isolated PTY for programmatic execution)
const build = await fetch(`${host('terminal')}/api/v1/terminal/execute?ephemeral=true`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ command: 'npm run build', wait: true })
});

// Log to database
await fetch(`${host('sqlite')}/api/v1/sqlite/db?db=logs&create_db_if_missing=true`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    transaction: [
      { statement: `INSERT INTO deploys (status, time) VALUES ('success', '${new Date().toISOString()}')` }
    ]
  })
});

// Send notification — `display` selects the target display ID ("0" = default)
await fetch(`${host('n')}/api/v1/notifications/notify`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ display: '0', summary: 'Deploy complete', body: 'Build succeeded' })
});

return { status: 'deployed', timestamp: new Date().toISOString() };
```

One script. Three services. Zero infrastructure. That's what HTTP composability looks like.


The `metadata` object is injected by hoody-exec. It includes `query` (URL params), `parameters` (dynamic route segments), `method`, `url`, `clientIp`, `projectId`, `containerId`, `nodeId`, `domain`, and more. See the [Exec documentation](/kit/exec/) for the full reference.


---

## What Just Happened?

You created a live API endpoint by writing a file. No `package.json`, no `npm install`, no `docker build`, no deploy command. The script's path (relative to the exec scripts directory) IS the URL path.

| Script Path | URL Path |
| :--- | :--- |
| `api/hello.js` | `/api/hello` |
| `api/users/list.js` | `/api/users/list` |
| `api/deploy.js` | `/api/deploy` |
| `webhooks/stripe.js` | `/webhooks/stripe` |

> **This is the HTTP revolution in practice.** Every script is an endpoint. Every endpoint is composable. Every service speaks HTTP. The platform disappears — what remains is just URLs.

**Next:** [The Hoody Kit →](/kit/)