How to Use ChatGPT to Build Node.js Backend APIs: Step-by-Step Guide with Codehooks.io
ChatGPT, GitHub Copilot, Cursor and other AI tools using Large Language Models (LLMs) can quickly generate a lot of code, but quite often the process is not straightforward and requires many iterations and a skilled developer to get the best results.
In this article we will show how a well-structured prompt can improve the AI-supported development effort.
What makes this guide special is that with Codehooks.io, you can deploy ChatGPT-generated code directly to production with minimal or no modifications and minimal setup. The combination of ChatGPT and Codehooks.io's "batteries included" approach means you can go from prompt to production-ready API and database in minutes.
This guide provides a practical prompt template that generates deployment-ready Codehooks.io code. Just copy the template, add your requirements, and let ChatGPT do the heavy lifting.
Best Practices for Prompting ChatGPT
To get the best results when generating code with ChatGPT for Codehooks.io, I tend to follow these principles:
- Provide Examples – Show the AI correct syntax so it doesn't invent one (included in the template).
- Be Explicit About Features – List key components (e.g., API routes, database interactions, worker queues).
- Include a Step-by-Step Structure – Help the model understand how the code should be structured.
- Instructions at the End – The most important details should come last in the prompt.
The ChatGPT / LLM Prompt Template
This template follows the principles above and is designed and tested to generate deployment-ready Codehooks.io code.
You are an expert in backend development using Codehooks.io. Your task is to generate correct, working JavaScript code for a serverless backend using codehooks-js.
Follow these rules:
- Use the `codehooks-js` package correctly.
- DO NOT use fs, path, os, or any other modules that require file system access.
- Create REST API endpoints using `app.get()`, `app.post()`, `app.put()`, and `app.delete()`.
- Use the built-in NoSQL document database via:
- `conn.insertOne(collection, document)`
- `conn.getOne(collection, ID | Query)`
- `conn.findOne(collection, ID | Query)`
- `conn.getMany(collection, query, options)` // **IMPORTANT:** This function returns a stream - add `.toArray()` to the end of the chain if you need to get out an array to manipulate data (sort, filter, map etc.)
- `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`: 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
- 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.
- Implement worker queues with `app.worker(queueName, workerFunction)` and enqueue tasks using `conn.enqueue(queueName, payload)`.
- 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.
- 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. Do not assume a module is globally available in Node.js.
- If a function requires a third-party library (e.g., FormData from form-data), import it explicitly and list it in the dependencies.
- Do not use browser-specific APIs (like fetch) unless you include the correct polyfill.
- 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';
const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
// 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 } from 'codehooks-js';
app.job('0 0 * * *', async () => {
console.log('Running scheduled task...');
res.end(); // done
});
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(),
});
app.crudlify({ customer: customerSchema });
// bind to serverless runtime
export default app.init();
```
**Complete REST API Example:**
```javascript
import { app, Datastore } from 'codehooks-js';
// Get all items using getMany with toArray() to get an array
app.get('/api/items', async (req, res) => {
const conn = await Datastore.open();
const items = await conn.getMany('items', {}).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, 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();
```
**Authentication Example:**
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
// 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();
}
});
// Auth hook for webhook endpoints
app.auth('/webhooks/*', (req, res, next) => {
// Validate webhook signature instead of JWT
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
if (validateWebhookSignature(payload, signature)) {
next(); // Allow webhook
} else {
res.status(401).json({ error: 'Invalid webhook signature' });
res.end();
}
});
function validateWebhookSignature(payload, signature) {
// Custom webhook validation logic using Node.js crypto module
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return signature === expectedSignature;
}
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
},
});
// 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();
```
For additional detailed information about the Codehooks.io platform, you can reference https://codehooks.io/llms.txt
[describe what you need here]
Insert Your Request to ChatGPT
To use this template effectively, insert your request at the bottom of the prompt. ChatGPT (and other LLMs) will use the provided examples to generate accurate Codehooks.io code.
For example:
I need an API that can receive orders from a client and send back a receipt in PDF using Mailgun's REST API. Required fields for the order are name, email, items (array of objects with name and price), total. You can use Yup for validation. Use a worker queue to generate the PDF and send the email.
Using the ChatGPT 4o model, this gave me the following working backend API:
import { app, Datastore } from 'codehooks-js';
import * as Yup from 'yup';
import PDFDocument from 'pdfkit';
import axios from 'axios';
import FormData from 'form-data';
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;
const MAILGUN_FROM = process.env.MAILGUN_FROM;
// Order validation schema
const orderSchema = Yup.object({
name: Yup.string().required(),
email: Yup.string().email().required(),
items: Yup.array()
.of(
Yup.object({
name: Yup.string().required(),
price: Yup.number().required().positive(),
})
)
.required(),
total: Yup.number().required().positive(),
});
// POST /orders - Create new order
app.post('/orders', async (req, res) => {
try {
// Validate order data
const order = await orderSchema.validate(req.body);
const conn = await Datastore.open();
const savedOrder = await conn.insertOne('orders', order);
// Enqueue worker to generate PDF and send email
await conn.enqueue('processOrder', {
orderId: savedOrder._id,
email: order.email,
});
res.json({
message: 'Order received, receipt will be emailed shortly.',
orderId: savedOrder._id,
});
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// Worker function to generate PDF and send email
app.worker('processOrder', async (req, res) => {
const { orderId, email } = req.body.payload;
const conn = await Datastore.open();
// Retrieve order from database
const order = await conn.getOne('orders', orderId);
if (!order) {
console.error('Order not found:', orderId);
return res.end();
}
// Generate PDF receipt
const pdfBuffer = await generateReceiptPDF(order);
// Send email with the receipt
await sendEmailWithAttachment(email, pdfBuffer, `Receipt_${orderId}.pdf`);
console.log('Receipt sent to:', email);
res.end();
});
// Function to generate PDF receipt
async function generateReceiptPDF(order) {
return new Promise((resolve, reject) => {
const doc = new PDFDocument();
const buffers = [];
doc.on('data', buffers.push.bind(buffers));
doc.on('end', () => resolve(Buffer.concat(buffers)));
doc.on('error', reject);
doc.fontSize(20).text('Order Receipt', { align: 'center' });
doc.moveDown();
doc.fontSize(14).text(`Name: ${order.name}`);
doc.text(`Email: ${order.email}`);
doc.moveDown();
doc.text('Items:');
order.items.forEach((item) => {
doc.text(`- ${item.name}: $${item.price.toFixed(2)}`);
});
doc.moveDown();
doc.text(`Total: $${order.total.toFixed(2)}`, { bold: true });
doc.end();
});
}
// Function to send email via Mailgun
async function sendEmailWithAttachment(to, pdfBuffer, filename) {
const formData = new FormData();
formData.append('from', MAILGUN_FROM);
formData.append('to', to);
formData.append('subject', 'Your Order Receipt');
formData.append('text', 'Please find attached your order receipt.');
formData.append('attachment', pdfBuffer, {
filename,
contentType: 'application/pdf',
});
try {
await axios.post(
`https://api.mailgun.net/v3/${MAILGUN_DOMAIN}/messages`,
formData,
{
auth: { username: 'api', password: MAILGUN_API_KEY },
headers: formData.getHeaders(),
}
);
} catch (err) {
console.error('Failed to send email:', err.response?.data || err.message);
}
}
// Initialize app
export default app.init();
Deploying the API with Codehooks.io
Deploying the new API with Codehooks.io is a straightforward process that allows you to quickly get your backend up and running. You can use either the CLI or the web-based Code Studio.
Using the web-based Codehooks Studio for development
Using the Codehooks Studio, you simply edit the package.json and the index.js files and replace their contents with the code from the ChatGPT prompt response. Click "NPM install" and "Deploy" and you are done (if it works, that is 😉). Also remember to set the environment variables in the Settings.
In the studio you can easily see database collections and documents, see live logs, change settings and more.
If you need to add files to source control or use CoPilot, Cursor or other AI IDEs, you can continue development on your local machine.
Just use the CLI command codehooks init
:
codehooks init
? Select project
analytix-yk1e
bsgenerator-94zl
demo-mlyg
❯ fideltech-ehyh
junglelab-5cqe
krypto-xogu
mongotest-mitn
(Move up and down to reveal more choices)
Select the project and space you used and the files will be downloaded to the current folder.
Using the Codehooks CLI
Follow these steps to deploy your API using the CLI and ensure it's configured correctly with the necessary environment variables.
Install Codehooks CLI
If you haven't already, install the Codehooks CLI by running:
npm install -g codehooks
(If you don't have a Codehooks.io account yet, you can create one at codehooks.io)
Create a new project
codehooks create order-api
Edit the files
Edit the index.js and package.json files and paste the code from the ChatGPT prompt response.
Install dependencies
npm install
Deploy Your Project
codehooks deploy
Set the environment variables
codehooks set-env MAILGUN_API_KEY <your-mailgun-api-key> --encrypted
codehooks set-env MAILGUN_DOMAIN <your-mailgun-domain>
codehooks set-env MAILGUN_FROM <your-email-address>
Test (or debug) your new API
You can test your API by sending requests to the /orders endpoint (you can use Postman or curl for example). To get the URL and curl examples run:
codehooks info --examples
Example curl request:
curl -X POST "https://lucky-zenith-2f2d.codehooks.io/orders" \
-H "Content-Type: application/json" \
-H "x-apikey: 5773d8fa-992e-4179-b653-d6daa7bf5e11" \
--data-raw '{
"name": "Emma Hansen",
"email": "[email protected]",
"items": [
{ "name": "Product A", "price": 29.99 },
{ "name": "Product B", "price": 49.99 }
],
"total": 79.98
}'
Example response:
{
"message": "Order received, receipt will be emailed shortly.",
"orderId": "66e94d786e3a3d0001a30e14"
}
Check the logs (and debug)
codehooks logs --follow
By following these steps, you can efficiently deploy the new API with Codehooks.io, ensuring that your application is secure and ready for production use. For use by a web client, you would need to add some form of authentication other than API keys.
Conclusion - A perfect solution ?
Well, no. But it's an improvement.
We've shown how to use a well-structured prompt to help ChatGPT to generate a non-trivial Node.js backend API with Codehooks.io. Being able to go from idea to (almost) production-ready API in minutes is a game changer.
The main Challenge of Prompting LLMs is consistency
Consistent output is not the norm when prompting LLMs. Given an identical prompt, ChatGPT might produce a different variant of the solution each time. It will probably work, but you would be wise to ask it why it made the choices it did. Providing a structured prompt will get you to a good starting point faster than just asking for code.
Well structured prompts provide real value
Good prompts provide real value and software developers with good prompting skills can be far more productive than those without. By following the template approach with codehooks.io, developers can quickly build and experiment with APIs.
Next Steps
- Copy the template and try it for your own use case.
- Modify the examples to fit your specific needs.
- Test and refine the LLM-generated code as necessary.
Happy coding! 🚀