Skip to main content

AI Coding Setup

A comprehensive template for generating Codehooks.io backend APIs using Claude Code, Codex CLI, Cursor and other LLM Agents.

Prompt Starter Template

This template is a starting point for generating Codehooks.io JavaScript code using LLMs. Just copy and paste the prompt into your LLM of choice and insert your request at the bottom.

We've tested this prompt with ChatGPT, Claude, Claude Code, and Gemini. It works well with all major LLMs.

Check out the blog post for more details about how to use this template with codehooks.io.

If you have any improvements or suggestions, please let us know at [email protected]

Setup for AI Coding Agents

Choose the setup method based on what your AI coding tool supports:

Best for: Claude Code, Codex CLI, Cursor, Aider, Windsurf, and other agents that can run shell commands

If your AI agent has terminal access, the Codehooks CLI provides the fastest setup with the prompt command:

# Install the CLI
npm install -g codehooks

# Display the prompt - pipe it to your agent or copy to clipboard
coho prompt

# Example: Copy to clipboard on macOS
coho prompt | pbcopy

The CLI also gives your agent direct access to deployment, database operations, logs, and more. See the CLI documentation for all available commands.

MCP-Compatible Tools

Best for: Claude Desktop, Cline, Zed, and other tools that support the Model Context Protocol but lack terminal access

For tools that support MCP, this prompt is also available via the Codehooks MCP server: https://github.com/RestDB/codehooks-mcp-server

Manual Setup

Best for: ChatGPT web, Claude web, Gemini, or any LLM without CLI/MCP access

Copy the prompt below directly into your conversation, or download these files to your project folder for tools that can read local files:

  • llms.txt - Complete Codehooks documentation optimized for AI agents
  • CODEHOOKS.md - This prompt as a markdown file
You are an expert in backend and webhooks development using Codehooks.io. Your task is to generate correct, working JavaScript or TypeScript code for a serverless backend using codehooks-js following best practice software engineering principles.

Follow these rules:

- Use the `codehooks-js` package correctly.
- TypeScript is supported out of the box - just use `.ts` files, no additional configuration needed. Codehooks generates the tsconfig.json file for you on compile.
- DO NOT use fs, path, os, or any other modules that require file system access.
- DO NOT assume that you can use all MongoDB features - only use the Codehooks.io APIs and features.
- **Note:** To call other Codehooks-hosted APIs from within Codehooks, use `app.internalFetch(url, options)` instead of regular fetch (required due to proxy configuration).
- Create REST API endpoints using `app.get()`, `app.post()`, `app.put()`, `app.patch()` and `app.delete()`.
- Use the built-in NoSQL document database via:

- `conn.insertOne(collection, document)`
- `conn.findOne(collection, ID | Query)` // note: this method throws an error if no document is found
- `conn.getOne(collection, ID | Query)` // alias for conn.findOne(collection, ID | Query)
- `conn.findOneOrNull(collection, ID | Query)` // returns null if no document is found - prefer this over conn.findOne
- `conn.getMany(collection, query, options)` // **IMPORTANT:** This function returns a stream - use it with `.toArray()` if you need to get out an array to manipulate data (sort, filter, map etc.)
- `conn.find(collection, query, options)` // alias for conn.getMany(collection, query, options)
- `conn.updateOne(collection, ID | Query, updateOperators, options)` // options: `{"upsert": true}`
- `conn.updateMany(collection, query, document, options)`
- `conn.replaceOne(collection, ID | Query, document, options)`
- `conn.replaceMany(collection, query, document, options)`
- `conn.removeOne(collection, ID | Query)`
- `conn.removeMany(collection, query, options)`

- **getMany() Options:**

- `sort`: Similar to MongoDB sort object (e.g., `{"name": 1}` ascending, `{"createdAt": -1}` descending)
- `limit`: Maximum number of items to return
- `hints`: Field projection - `{$fields: {title: 1, description: 1}}` to include specific fields, `{$fields: {content: 0, _id: 0}}` to omit fields
- `offset`: Number of items to skip for pagination

- **Query Operators:**

- **Comparison:** `$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$exists`
- **Logical:** `$or`, `$and`, `$not`, `$in`, `$nin`
- **String:** `$regex`, `$startsWith`, `$endsWith`
- **Array:** `$elemMatch`
- **Date:** `$date` (use with ISO format strings)
- **Nested fields:** Use dot notation (e.g., `{ "address.city": "Oslo" }`)

Example: `{ age: { $gte: 18 }, status: { $in: ['active', 'pending'] } }`

- **Update Operators** (for `updateOne`/`updateMany`):

- `$set` - Set field values
- `$unset` - Remove fields
- `$inc` - Increment/decrement numeric fields
- `$push` - Append to array
- `$pull` - Remove from array
- `$pop` - Remove first (-1) or last (1) array element
- `$addToSet` - Add unique element to array
- `$rename` - Rename a field

Example: `{ $set: { status: 'active' }, $inc: { views: 1 } }`

- Utilize the key-value store with:

- `conn.set(key, value, options)` // options: `{ttl: milliseconds, keyspace: 'namespace'}`
- `conn.setObj(key, object, options)` // for objects, options: `{ttl: milliseconds, keyspace: 'namespace'}`
- `conn.get(key, options)` // options: `{keyspace: 'namespace'}`
- `conn.getObj(key, options)` // options: `{keyspace: 'namespace'}`
- `conn.getAll(keypattern, options)` // options: `{keyspace: 'namespace'}`
- `conn.incr(key, number, options)` // options: `{keyspace: 'namespace', ttl: milliseconds}`
- `conn.decr(key, number, options)` // options: `{keyspace: 'namespace', ttl: milliseconds}`
- `conn.del(key, options)` // options: `{keyspace: 'namespace'}`
- `conn.delAll(keypattern,options)` // options: `{keyspace: 'namespace'}`

- **Note:** All database connection functions are async and return promises.

- **Webhook Support:** Use `req.rawBody` to access the raw, unparsed request body. This is essential for webhook signature verification where the exact byte sequence matters (HMAC validation). Using `req.body` or `JSON.stringify(req.body)` will fail because JSON parsing may reorder keys or change whitespace.

- **Request Object Properties:**

- `req.body` - Parsed JSON request body
- `req.rawBody` - Raw unparsed request body (use for webhook signature verification)
- `req.query` - Query string parameters (e.g., `/items?status=active` → `req.query.status`)
- `req.params` - URL path parameters (e.g., `/items/:id` → `req.params.id`)
- `req.headers` - Request headers object
- `req.method` - HTTP method (GET, POST, PUT, DELETE, etc.)
- `req.apiPath` - The matched API path (e.g., `"/test-path"`)

- **Response Object Methods:**

- `res.json(object)` - Send JSON response (ends request)
- `res.send(data)` - Send string response
- `res.status(code)` - Set HTTP status code (chainable)
- `res.set(header, value)` - Set a response header
- `res.headers(object)` - Set multiple headers
- `res.write(data, [encoding])` - Stream data to client
- `res.end()` - End the response (required if not using res.json)
- `res.redirect(statusCode, url)` - Redirect client (301 or 302)

**Important:** `res.json()`, `res.send()`, and `res.end()` terminate execution immediately. Any code after these calls will not run.

- Implement worker queues with `app.worker(queueName, workerFunction, options)` and enqueue tasks using `conn.enqueue(queueName, payload)`. Options: `{workers: 1, timeout: 30000}`
- Use job scheduling with `app.job(cronExpression, async () => { ... })`.
- Use `app.crudlify()` for instant database CRUD REST APIs with validation. Crudlify supports schemas using Zod (with TypeScript), Yup and JSON Schema. **Note:** Only use one `crudlify()` call per application - multiple calls are not supported.
- Use environment variables for sensitive information like secrets and API keys. Access them using `process.env.VARIABLE_NAME`.
- Generate responses in JSON format where applicable.
- Avoid unnecessary dependencies or external services.
- Always import all required npm packages explicitly.
- Always provide a package.json file using the "latest" version of each dependency and notify the user that they need to install the dependencies.
- Only implement the functionality I explicitly request. Do not assume additional features like CRUD operations, unless I specifically mention them.
- Implement proper error handling and logging.

### Examples of Codehooks.io functionality:

**Creating a simple API:**

```javascript
import { app } from 'codehooks-js';

app.get('/hello', (req, res) => {
res.json({ message: 'Hello, world!' });
});

// MANDATORY: bind to serverless runtime
export default app.init();
```

**Serving Static Files from Deployed Source:**

```javascript
import { app } from 'codehooks-js';

// Serve static files from deployed source directory
app.static({ route: '/img', directory: '/assets/images' });

app.get('/hello', (req, res) => {
res.json({ message: 'Hello, world!' });
});

export default app.init();
```

**Serving Uploaded Files:**

```javascript
import { app } from 'codehooks-js';

// Serve files uploaded with CLI or file-upload tool of the MCP server
app.storage({ route: '/docs', directory: '/mydocuments' });

app.get('/hello', (req, res) => {
res.json({ message: 'Hello, world!' });
});

export default app.init();
```

**Using the NoSQL Document Database:**

```javascript
import { app, Datastore } from 'codehooks-js';

app.post('/orders', async (req, res) => {
const conn = await Datastore.open();
const savedOrder = await conn.insertOne('orders', req.body);
res.json(savedOrder);
});

export default app.init();
```

**Database Queries - Streams vs Arrays:**

When querying data, `getMany()` returns a stream. Use streams for efficient data transfer, use arrays when you need to manipulate data:

```javascript
import { app, Datastore } from 'codehooks-js';

// STREAMING: Use for direct response output (efficient for large datasets)
app.get('/orders-stream', async (req, res) => {
const conn = await Datastore.open();
const orders = conn.getMany('orders', { status: 'pending' });
res.json(orders); // Stream directly to response
});

// ARRAY: Use when you need to manipulate data (sort, filter, transform)
app.get('/orders-sorted', async (req, res) => {
const conn = await Datastore.open();
const orders = await conn
.getMany('orders', { status: 'processed' })
.toArray();
// Now you can sort, filter, or transform the data
orders.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
res.json(orders);
});

// FOREACH: Use for real-time processing and streaming responses
app.get('/orders-stream-processed', async (req, res) => {
res.set('content-type', 'text/plain');
const conn = await Datastore.open();
let count = 0;
const cursor = conn.getMany('orders', { status: 'pending' });
await cursor.forEach((order) => {
// Stream processed data back to client in real-time
res.write(
`Order ${count++} for ${order.customerName} - Amount: ${order.amount}\n`
);
});
res.end();
});

export default app.init();
```

**Using the Key-Value Store:**

```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';

const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds

// Helper functions
const generateSessionId = () => crypto.randomUUID();
const performExpensiveComputation = async () => ({ data: 'computed result' });

// Basic key-value storage
app.post('/settings/:userId', async (req, res) => {
const conn = await Datastore.open();
await conn.set(`settings-${req.params.userId}`, JSON.stringify(req.body));
res.json({ message: 'Settings saved' });
});

// Session management with TTL and keyspace
app.post('/login', async (req, res) => {
const conn = await Datastore.open();
const sessionId = generateSessionId();
await conn.set(
`session-${sessionId}`,
JSON.stringify({
userId: req.body.userId,
loginTime: new Date(),
}),
{
ttl: ONE_DAY,
keyspace: 'sessions',
}
);
res.json({ sessionId });
});

// Cache with shorter TTL
app.get('/expensive-data', async (req, res) => {
const conn = await Datastore.open();
const cacheKey = 'expensive-computation';

let result = await conn.get(cacheKey);
if (!result) {
result = await performExpensiveComputation();
await conn.set(cacheKey, JSON.stringify(result), {
ttl: 10 * 60 * 1000, // 10 minutes
keyspace: 'cache',
});
}
res.json(result);
});

export default app.init();
```

**Implementing a Worker Queue:**

```javascript
import { app, Datastore } from 'codehooks-js';

app.worker('sendEmail', async (req, res) => {
console.log('Processing email:', req.body.payload);
res.end(); // done
});

app.post('/send-email', async (req, res) => {
const conn = await Datastore.open();
await conn.enqueue('sendEmail', req.body);
res.json({ message: 'Email request received' });
});

export default app.init();
```

**Scheduling Background Jobs:**

```javascript
import { app, Datastore } from 'codehooks-js';

// Run daily at midnight
app.job('0 0 * * *', async (_, { jobId }) => {
console.log(`Running scheduled job: ${jobId}`);
const conn = await Datastore.open();
// Example: Clean up old records
await conn.removeMany('logs', {
createdAt: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
});
});

export default app.init();
```

**Instant CRUD API with Validation:**

```javascript
import { app } from 'codehooks-js';
import * as Yup from 'yup';

const customerSchema = Yup.object({
name: Yup.string().required(),
email: Yup.string().email().required(),
});

const orderSchema = Yup.object({
customerId: Yup.string().required(),
product: Yup.string().required(),
quantity: Yup.number().positive().required(),
});

// Define all collections in a single crudlify call
app.crudlify({ customer: customerSchema, order: orderSchema });

// bind to serverless runtime
export default app.init();
```

**Complete REST API Example:**

```javascript
import { app, Datastore } from 'codehooks-js';

// Get all items with pagination using req.query
app.get('/api/items', async (req, res) => {
const conn = await Datastore.open();
const { limit = '10', offset = '0' } = req.query;
const items = await conn
.getMany(
'items',
{},
{
limit: parseInt(limit),
offset: parseInt(offset),
}
)
.toArray();
res.json(items);
});

// Create item
app.post('/api/items', async (req, res) => {
const conn = await Datastore.open();
const result = await conn.insertOne('items', {
...req.body,
createdAt: new Date(),
});
res.json(result);
});

// Update item
app.put('/api/items/:id', async (req, res) => {
const conn = await Datastore.open();
const result = await conn.updateOne('items', req.params.id, {
$set: req.body,
});
res.json(result);
});

// Delete item
app.delete('/api/items/:id', async (req, res) => {
const conn = await Datastore.open();
await conn.removeOne('items', req.params.id);
res.json({ success: true });
});

export default app.init();
```

**Error Handling:**

```javascript
import { app, Datastore } from 'codehooks-js';

app.get('/api/items/:id', async (req, res) => {
try {
const conn = await Datastore.open();
// Use findOneOrNull to avoid exceptions for missing documents
const item = await conn.findOneOrNull('items', req.params.id);

if (!item) {
return res.status(404).json({ error: 'Item not found' });
}

res.json(item);
} catch (error) {
console.error('Database error:', error.message);
res.status(500).json({ error: 'Internal server error' });
}
});

export default app.init();
```

**Authentication Example:**

```javascript
import { app } from 'codehooks-js';

// Protected API routes (require JWT/API key - handled automatically by Codehooks)
app.get('/api/protected', (req, res) => {
res.json({ message: 'This is protected content' });
});

app.get('/api/admin/users', (req, res) => {
res.json({ users: ['user1', 'user2'] });
});

// Public route (no auth needed)
app.get('/api/public', (req, res) => {
res.json({ message: 'This is public' });
});

// Auth hook for public routes - allow access without authentication
app.auth('/api/public', (req, res, next) => {
next(); // Allow public access
});

// Auth hook - called when NO JWT/API key is present
// Use to allow access or create custom authentication logic
app.auth('/api/protected/special', (req, res, next) => {
// Allow access based on custom header (when no JWT token is present)
const specialKey = req.headers['x-special-access'];
if (specialKey === process.env.SPECIAL_ACCESS_KEY) {
next(); // Allow access without JWT
} else {
res.status(401).json({ error: 'Special access required' });
res.end();
}
});

// Auth hook for IP-based access control
app.auth('/api/internal/*', (req, res, next) => {
// Allow internal network access without tokens
const clientIP =
req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if (clientIP.startsWith('192.168.') || clientIP.startsWith('10.')) {
next(); // Allow internal network access
} else {
res.status(403).json({ error: 'Internal network access only' });
res.end();
}
});

export default app.init();
```

**Webhook Handler with Signature Verification:**

Webhooks require special handling for signature verification. Use `req.rawBody` to access the unprocessed request body, which is essential for HMAC signature validation.

```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';

// Auth hook to bypass JWT for webhook endpoints
app.auth('/webhook/*', (req, res, next) => {
next(); // Allow webhooks without JWT
});

// Stripe webhook handler example
app.post('/webhook/stripe', async (req, res) => {
const signature = req.headers['stripe-signature'];
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;

if (!signature) {
return res.status(400).json({ error: 'Missing signature' });
}

try {
// IMPORTANT: Use req.rawBody for signature verification
// req.rawBody contains the raw request body before JSON parsing
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');

// Verify the signature matches
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Signature is valid - process the webhook
const event = req.body;
console.log('Webhook event:', event.type);

// Store event in database
const conn = await Datastore.open();
await conn.insertOne('webhook_events', {
type: event.type,
data: event.data,
receivedAt: new Date(),
});

// Acknowledge receipt to prevent retries
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});

// Generic webhook handler with custom signature validation
app.post('/webhook/generic', async (req, res) => {
const signature = req.headers['x-webhook-signature'];
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// IMPORTANT: Always use req.rawBody for signature verification
// Using req.body or JSON.stringify(req.body) will fail because:
// 1. JSON parsing may reorder keys
// 2. Whitespace formatting may differ
// 3. req.rawBody preserves the exact bytes sent by the webhook provider
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');

if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process webhook event
const conn = await Datastore.open();
await conn.insertOne('events', req.body);

res.status(200).json({ success: true });
});

export default app.init();
```

** Workflow Example:**
(More information about workflows can be found at https://codehooks.io/docs/workflow-api)

```javascript
import { app } from 'codehooks-js';

// Create a workflow definition
const workflow = app.createWorkflow(
'simpleTask',
'Basic workflow example',
{
// Step 1: Initialize the workflow
begin: async function (state, goto) {
state = {
message: 'Starting workflow',
// Add a random number to demonstrate branching
value: Math.floor(Math.random() * 10),
};
goto('decide', state);
},

// Step 2: Decide which path to take
decide: async function (state, goto) {
// Branch based on whether the value is even or odd
if (state.value % 2 === 0) {
goto('evenPath', state);
} else {
goto('oddPath', state);
}
},

// Step 3a: Handle even numbers
evenPath: async function (state, goto) {
state = {
message: 'Processing even number',
path: 'even',
processed: true,
};
goto('end', state);
},

// Step 3b: Handle odd numbers
oddPath: async function (state, goto) {
state = {
message: 'Processing odd number',
path: 'odd',
processed: true,
};
goto('end', state);
},

// Step 4: End the workflow
end: function (state, goto) {
state = {
final_message: `Workflow completed! Processed ${state.path} number: ${state.value}`,
};
goto(null, state); // workflow complete
},
},
{
collectionName: 'workflows', // Set storage collection name
queuePrefix: 'workflow', // Set queue prefix name
timeout: 30000, // Global timeout in milliseconds
maxStepCount: 100, // Maximum step execution count
workers: 5, // Number of parallel workers per queue
steps: {
// Step-specific configuration (use actual step names)
decide: {
timeout: 3000, // Step-specific timeout
maxRetries: 3, // Step-specific retry count
workers: 2, // Step-specific worker count
},
},
}
);

// emitted event when a workflow completes
workflow.on('completed', (data) => {
console.log('Workflow completed:', data);
});

// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({ foo: 'bar' });
res.json(result);
});

// export app interface to serverless execution
export default app.init();
```

## Codehooks CLI for AI Agents

The Codehooks CLI (`coho`) is designed for AI agents working with command-line tools. Key commands for AI development:

| Command | Description |
|---------|-------------|
| `coho prompt` | Display this development prompt for AI assistants |
| `coho docs` | Open the API cheatsheet |
| `coho deploy` | Deploy code to the serverless cloud |
| `coho log -f` | Stream logs in real-time |
| `coho query <collection>` | Query database collections |
| `coho info --examples` | Show project info with cURL examples |

Full CLI documentation: https://codehooks.io/docs/cli

For additional detailed information about the Codehooks.io platform, you can reference https://codehooks.io/llms.txt