REST (representational state transfer) is an architectural style, a set of rules and guidelines about how you should build a web API. Those rules are architectural constraints that build on existing network protocols, and an API that follows them is called a REST API.
1. Intro
1.1. What is REST API
REST principles are:
- Stateless: each API request should contain all the information needed to process the request since the server has no state in this regard
- Client-server architecture: both the server and the client are decoupled and can pivot independently; between them many layers may interfere
- Uniform communications : you have to comply to standard HTTP methods (GET, POST, PUT, DELETE)
- Resources : everything is treated as a resource. Ex: /user/123 it might be a user with id 123
- Standard format : resources can be represented in various formats like JSON, XML or HTML and the type can be specified in header
Why REST is common in web development:
- Flexibility and Granularity A clear separation of concerns lets multiple teams work on different parts of the same project at the same time
- Interoperability the same resources can be consumed by a lot of clients like web-browsers, mobile apps, servers
- Scalability Because REST APIs are stateless, they can efficiently handle a large number of concurrent requests and easily scale up or down.
1.2. Node.js and Non-Blocking I/O
Node.js is an asynchronous, event-driven JavaScript runtime built on Chrome's V8 engine, designed for building scalable applications. In this context, scalability is closely related to the event loop and the non-blocking I/O model. Most I/O operations don't block the main thread. Node.js hands them to libuv, which uses the operating system's async I/O (or a thread pool for some operations), and when an operation completes, its callback runs on the main thread. That lets Node.js keep accepting requests while I/O is pending, so the application can handle many simultaneous connections. This makes Node.js a good fit for live-streaming, online gaming, real-time apps and microservices, and it is also more energy-efficient than managing multiple threads.
1.3. Express.js
Express.js provides a minimalist framework that simplifies the process of building web applications and APIs. Its syntax is straightforward and easy to understand, also for beginners. It also comes with a middleware architecture, powerful routing, easy integration of a variety of modules, a lightweight core and flexibility in the patterns you use. It is widely adopted and has strong community support.
1.4. Project
In this article, I'll build an API to manage digital agents using Node.js and Express. Through this project, you'll learn how to design a scalable RESTful API, set up endpoints, integrate middleware, and apply best practices for scalability.
- GET /agents : List all AI agents.
- POST /agents : Create a new AI agent.
- GET /agents/:id : Retrieve details about a specific agent.
- PUT /agents/:id : Update an agent’s attributes (e.g., status, current task).
- DELETE /agents/:id : Remove an agent.
- POST /agents/:id/execute : Instruct an agent to perform a task, simulating an AI action.
What you'll learn:
- How to set up a Node.js project with Express.
- Designing RESTful endpoints for CRUD operations.
- Integrating middleware for logging, error handling, and security.
- Scalability tips to handle numerous concurrent requests.
2. Key Concepts
2.1. Express Basics
As mentioned on the official website "Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications." Minimal and flexible means that Express, when installed comes packed with base core functionality and the user has to add the extra modules as required by the specific of the project. So, in order to handle logging, headers,session, cookies, authentication etc., you have to install them extra.
Express greatly simplifies working with HTTP in Node.js by abstracting away many of the low-level details you’d otherwise have to manage with the native http module. You don't parse request URLs and query strings by hand or set every response header yourself, so most of the code you write is your application’s logic. Key Advantages:
Simplified Routing
Express offers methods like app.get(), app.post(), app.put(), and app.delete(), allowing you to define routes in a straightforward manner. This eliminates the need for tedious URL parsing and manual method checks, making your code more readable and maintainable.
Built-In Middleware Support
The middleware architecture in Express lets you plug in functions for common tasks, so you don't have to build them yourself. Parsing JSON or URL-encoded bodies works this way, and so do request logging and cookie or session handling. You add what your project needs and the core server stays lean.
Enhanced Readability and Maintainability
Express encourages a clean separation of concerns. In the project below the routes live in their own file (routes/agents.js) and errors go through one error-handling middleware, which keeps the code easier to understand and maintain.
Rich Ecosystem
Thanks to its widespread adoption, Express has a vast ecosystem of plugins and middleware available. For authentication or logging there is usually a well-supported package already. Morgan, which this article uses for logging, is one of them.
The next two sections look at routing and middleware in more detail.
2.2. Express (basic) routing
Routing in Express refers to how an application’s endpoints (or routes) respond to client requests. Each route is associated with an HTTP method and a specific URL pattern. Express makes it easy to define routes using methods like app.get(), app.post(), app.put(), and app.delete().
For example, consider a simple route for retrieving digital agents:
app.get("/agents", (req, res) => {
// Logic to retrieve and return a list of agents
res.send("List of agents")
})
/**
* in this snnipet, a GET request to /agents triggers the callback function, which sends back a list of agents.
* This abstraction hides the underlying complexity of URL parsing and method handling that you’d otherwise manage
* with Node’s native HTTP module.
* */2.3. Middleware (basic)
Middleware functions in Express are at the heart of request processing. They have access to the request object (req), the response object (res), and the next middleware function in the chain (next). Middleware can perform tasks such as:
- Logging requests
- Parsing request bodies (e.g., JSON or URL-encoded data)
- Handling authentication and authorization
- Managing sessions or cookies
- Error handling
app.use((req, res, next) => {
console.log(`Incoming ${req.method} request to ${req.url}`);
next(); // Pass control to the next middleware or route handler
});
2.4 Request-Response cycle
The request-response cycle in an Express application follows these steps:
Request Received
The Node.js HTTP module accepts an incoming HTTP request and passes it to the Express application.
Middleware Processing
Express processes the request through a chain of middleware functions. Each middleware function can modify or validate the request before passing control onward. Other middleware can log it.
Routing
After all applicable middleware have run, Express matches the request to a route based on the HTTP method and URL. The corresponding route handler is then executed.
Route Handler Execution
The route handler retrieves the data it needs. Then it processes the input and prepares a response.
Response Sent
Finally, Express sends the HTTP response back to the client.
2.5. State in API REST
As I mentioned above, a cornerstone of REST is that each API request is stateless. This means:
- Self-Contained Requests
Every request from the client must include all the information the server needs to understand and process it. The server does not store any context between requests. For example, if a client wants to update an agent, the request must contain all the necessary data and authentication details. - Benefits of Statelessness
- Scalability Since the server doesn't need to retain session state, it's easier to scale horizontally, because each request can be handled by any available server instance.
- Reliability Stateless systems are simpler to design and troubleshoot because there’s no hidden context that could lead to inconsistent behavior.
- Caching Since each request is self-contained, responses can be more easily cached, improving performance.
2.6. Versioning
As your API evolves, changes might break compatibility with older client applications. A versioning system lets you introduce new features or improvements without disrupting existing clients, and it allows a gradual transition where clients can upgrade at their own pace. Common version strategies:
- URI Versioning Include the version in the URL, such as /v1/agents or /api/v1/agents
- Header Versioning Specify the API version in a custom HTTP header (e.g., Accept: application/vnd.myapi.v1+json).
- Query Parameters Pass the version as a parameter in the query string, e.g., /agents?version=1.
Having a version system in place allows older clients to rely on the old version and keep up their systems and allows you to easily introduce new features without forcing all the clients to upgrade immediately.
3. Project Setup
3.1 Handling Endpoints
As mentioned earlier, in this article, we'll build an API to manage digital agents using Node.js and Express. We'll start by initializing a new Node/Express project and installing the necessary modules to showcase the advantages of the Express.js framework.
Steps:
- Prerequisites: Ensure you have Node.js installed (which includes NPM). If you don't, follow the NPM installation guide.
- Init the project: Open your terminal, navigate to your desired folder, and run:
npm initand follow the instructions in terminal(choose your desired entry point, I will stick toindex.js) - install Express.js : run
npm install expressin the same folder you initiate the project at step 2 - extra packages :
npm install --save-dev nodemonandnpm install --save morgan helmet, nodemon for automatic server restarts(so development purposes), morgan for logging http requests and helmet for setting secure HTTP headers. - optional packages:
npm install typescriptto have the ability to add types for the project
You should probably have this project file structure. Other folders/files will be added as I progress with the project.(routes/controllers, etc.)
-package.json
-index.jsand add this code to your entry file (index.js in my case).
const express = require('express');
const app = express()
const port = 3000
app.use(express.json());
// respond with "hello world" when a GET request is made to the homepage
app.get('/', (req, res) => {
res.send('Welcome to AI agents app')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
In the code above, we create an Express application instance and define a simple GET route for the homepage that returns "Welcome to AI agents app." Finally, we start the server on port 3000. If you open a web browser and navigate to http://localhost:3000, you should see the welcome message
4. Designing Endpoints
As mentioned above we'll try to cover the main http methods that you will be going to use most of the time. These are the routes that the express.js server will serve:
- GET /agents : List all AI agents.
- POST /agents : Create a new AI agent.
- GET /agents/:id : Retrieve details about a specific agent.
- PUT /agents/:id : Update an agent’s attributes (e.g., status, current task).
- DELETE /agents/:id : Remove an agent.
- POST /agents/:id/execute : Instruct an agent to perform a task, simulating an AI action.
At the root of your project create a folder routes and inside the agents.js file where we will store the handlers for each route method.
You should end up with a file like this:
// /routes/agents.js
const express = require('express');
const router = express.Router();
// GET /agents - List all AI agents
router.get('/agents', (req, res, next) => {
// Logic to retrieve all agents
res.send('List of all AI agents');
});
// POST /agents - Create a new AI agent
router.post('/agents', (req, res, next) => {
// Logic to create a new agent
res.send('New AI agent created');
});
// GET /agents/:id - Retrieve details about a specific agent
router.get('/agents/:id', (req, res, next) => {
const { id } = req.params;
// Logic to retrieve agent details by id
res.send(`Details of agent ${id}`);
});
// PUT /agents/:id - Update an agent's attributes
router.put('/agents/:id', (req, res, next) => {
const { id } = req.params;
// Logic to update the agent's details
res.send(`Agent ${id} updated`);
});
// DELETE /agents/:id - Remove an agent
router.delete('/agents/:id', (req, res, next) => {
const { id } = req.params;
// Logic to remove the agent
res.send(`Agent ${id} deleted`);
});
// POST /agents/:id/execute - Instruct an agent to perform a task
router.post('/agents/:id/execute', (req, res, next) => {
const { id } = req.params;
// Logic to execute a task for the agent
res.send(`Agent ${id} is executing a task`);
});
module.exports = router;
// and import this into the main index.js
//index.js
const agentsRouter = require('./routes/agents');
//it is common to prefix it with "api" to have a clear separation of concerns
app.use('/api', agentsRouter);An agent will be defined as an object with these keys:
{
"id": "a1b2c3",
"name": "Agent Smith",
"status": "idle",
"currentTask": null
}
If you run node index.js and visit the http://localhost:3000, you should see the same message("Welcome to AI agents app").
3.2 Middleware Integration
From the official docs "Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle. The next function is a function in the Express router which, when invoked, executes the middleware succeeding the current middleware."
3.2.1 Logging Middleware with Morgan
Morgan is a popular middleware for logging HTTP requests in Express. It helps you log details such as the request method, URL, status code, and response time. This is especially useful during development and debugging. We already installed Morgan. Now let's integrate it into our application.
//index.js
const morgan = require('morgan'); // Import Morgan
// Use morgan middleware to log all HTTP requests in the 'combined' format
// other formats available 'dev' & 'short'
app.use(morgan('combined'));With this setup, every HTTP request to your server will be logged in the 'combined' format.
In Express, middleware is executed in the order it's added. If a route handler sends a response, any middleware added after that handler will not be executed. Consider the following examples:
In your index.js:
//correct order to execute morgan -> navigate to /api/agents
app.use(morgan('combined'));
app.use('/api', agentsRouter);
app.get('/', (req, res) => {
res.send('Welcome to AI agents app')
})
//morgan middleware unreachable -> navigate to /api/agents
//in this case the api/router -> will trigger route handler GET agents and will run
// "res.send" which will kill all the middlewars that may be added after it
app.use('/api', agentsRouter);
app.use(morgan('combined'));
app.get('/', (req, res) => {
res.send('Welcome to AI agents app')
})Register anything that must run for every request, such as Morgan's logging, before the routers.
3.2.2 Error Handling Middleware
Error handling middleware in Express provides a centralized way to catch and process errors that occur during the request-response cycle. You can define a single middleware function that deals with errors uniformly, so your route handlers don't need their own error-handling code. That keeps the code simpler and easier to maintain. Centralized error handling gives you:
- Consistency: All errors are processed in one place, ensuring that your API returns consistent responses for errors.
- Simplified Debugging:Central logging of error details (like stack traces) makes it easier to diagnose issues.
- Separation of Concerns: Keeps your route handlers focused on their main logic without cluttering them with repetitive error-handling code.
Implementation:
// In your index.js
//place your error middleware last, so it doesn't interfere with other middleware
app.use((err, req, res, next) => {
console.error(err.stack);
// Send a generic error response
res.status(500).json({ error: 'Something went wrong!' });
});How It Works:
- When an error occurs in any of your route handlers or middleware, you call next(err) with the error object.
- Express will skip any remaining non-error-handling middleware and pass the error to your error-handling middleware.
- The error-handling middleware logs the error and sends a standardized HTTP response (in this case, a 500 Internal Server Error).
With this in place, any error passed to next(err) gets the same 500 response with the same JSON body.
3.2.3 Security Middleware with Helmet
Helmet is a collection of Express middleware that sets security-related HTTP headers. It helps protect your app from well-known web vulnerabilities by setting various HTTP headers appropriately. Helmet sets multiple HTTP headers to improve security, including:
- X-DNS-Prefetch-Control: Controls browser DNS prefetching.
- Strict-Transport-Security: Enforces secure (HTTPS) connections to the server.
- X-Frame-Options: Protects against click-jacking by controlling whether your content can be displayed in a frame.
- X-Content-Type-Options: Prevents browsers from MIME-sniffing a response away from the declared content-type.
- X-XSS-Protection: Enables the browser’s built-in cross-site scripting filters.
By automatically setting these headers, Helmet provides a layer of protection against common attacks, such as cross-site scripting (XSS) and clickjacking, without much configuration on your part. Integrating Helmet:
//index.js
const helmet = require('helmet');
app.use(helmet());Helmet should be placed early in your middleware stack to ensure that every response has secure HTTP headers applied. For now, we'll focus on Helmet as our primary security middleware, and we’ll cover the other practices (input validation, authentication, rate limiting, etc.) in upcoming chapters. Now that we have managed to understand and install the main middlewares you should end up with an index.js like this:
Flow Summary
In our Express application, middleware is executed sequentially, so the order in which you apply them matters.
- Logging Middleware (Morgan): Morgan is placed first to ensure every incoming HTTP request is logged. It captures the request method, URL, status code, and response time, which helps during development and debugging.
- Security Middleware (Helmet):Next, Helmet is applied early in the middleware stack. This middleware automatically sets various HTTP headers that help protect your application from common vulnerabilities such as cross-site scripting (XSS) and clickjacking.
- Routes and Application Logic:With logging and security in place, your application's routes (e.g., those handling AI agents) are mounted. When a request reaches the route handlers, it has been logged and Helmet's security headers are already set on the response.
- Error Handling Middleware:Finally, the error-handling middleware is added at the end of the middleware chain. This ensures that if any part of the request-response cycle encounters an error, it will be caught and handled in one place, which gives consistent error responses and logs the error details for debugging.
The final index.js below applies these middleware in that order.
const express = require('express')
const app = express()
const port = 3000
const morgan = require('morgan');
const agentsRouter = require('./routes/agents');
const helmet = require('helmet');
app.use(morgan('combined'));
app.use(helmet());
app.use('/api', agentsRouter);
app.get('/', (req, res) => {
res.send('Welcome to AI agents app')
})
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
4. Scalability Tips
As the name of this article suggests, the focus here is on scaling a Node.js/Express.js app. In this section we will discuss caching strategies, load balancing and other performance optimization
4.1 Caching Strategies
Caching means storing the results of expensive operations, like frequent database queries, in a temporary storage area (the cache), so that subsequent requests can be served quickly without hitting the database every time. It improves both the performance and the scalability of a RESTful API.
4.1.1 Caching advantages
- Reduced Database Load:By keeping frequently requested data in memory, caching significantly decreases the number of direct database calls. Responses come back faster, and the database server has more room left for complex queries or high load.
- Improved Response Times:Accessing data from memory is much faster than fetching it from disk or executing a database query. This matters most for high-traffic applications.
- Cost Efficiency:Reducing database interactions can help lower your operational costs, especially when using cloud-based databases that charge based on the number of read/write operations.
4.1.2 Types of Caching Strategies
- In-Memory Caching: This type of caching stores data in the server's memory, allowing for rapid access. Common tools for in-memory caching in Node.js include Redis and Node-Cache.
- Use Case: Ideal for caching small amounts of data that need to be accessed frequently.
- Example: Storing user session data or frequently accessed configuration settings.
- HTTP Caching: This involves caching responses on the client-side or intermediate proxies, reducing the need to repeatedly fetch the same data from the server.
- Use Case: Suitable for static assets like images, stylesheets, and scripts.
- Example: Setting HTTP headers such as Cache-Control and ETag to manage client-side caching.
- Database Caching: This strategy involves caching database query results, reducing the need to repeatedly execute the same queries.
- Use Case: Useful for read-heavy applications with frequent database queries.
- Example: Using a tool like Redis to cache the results of complex database queries.
- Application-Level Caching: This approach caches the output of specific functions or API calls within the application.
- Use Case: Effective for caching expensive computations or third-party API responses.
- Example: Caching the results of a weather API call that doesn't change frequently.
4.1.3 Example: In memory caching with Redis (will update the current ai-agents project)
- install redis
npm install redis - run "redis db" for simplicity I will run it in a docker container
docker run --name my-redis -p 6379:6379 -d redis- set up a redis client module
const redis = require('redis');
const redisClient = redis.createClient();
redisClient.on('error', (err) => console.error('Redis Client Error', err));
(async () => {
await redisClient.connect();
})();
module.exports = redisClient;- create a cache middleware
//caching-middleware/caching.js
const redisClient = require('../redisClient');
async function cache(req, res, next) {
const { id } = req.params; // Adjust based on your route parameters
try {
const cachedData = await redisClient.get(`data:${id}`);
if (cachedData) {
console.log(`Cache hit for agent ${id}`);
return res.json(JSON.parse(cachedData));
}
next();
} catch (err) {
console.error(err);
next();
}
}
module.exports = cache;
- Cache Data: Cache the data after processing the request if it is not already cached.
router.get('/agents/:id', cache, async (req, res, next) => {
const { id } = req.params;
const dataFromDB = { id, name: 'Example Data' };
// Store the result in Redis with a TTL (time to live) of 3600 seconds
//each time a request is made to this route the middleware check if the data is cached and if so
//will return it, if not will make the db call
await redisClient.setEx(`data:${id}`, 3600, JSON.stringify(dataFromDB));
res.json(dataFromDB);
});4.1.4 Best Practices for Caching
- Cache Invalidation: Ensure that stale or outdated data is invalidated and removed from the cache to maintain data consistency. This might include time-to-live (TTL) settings or event-driven updates to clear or refresh cache entries when the underlying data changes.
- Cache Granularity: Cache data at the appropriate level of granularity. Too fine-grained caching can lead to overhead, while too coarse-grained caching may reduce cache effectiveness.
- Monitoring and Metrics: Implement monitoring and logging to track cache hits, misses, and performance metrics, allowing for continuous optimization. Tools like Redis' built-in monitoring features can help you understand how effectively your caching strategy is reducing the load on your database.
4.2 Load balancing
Load balancing matters a lot for scaling Node.js and Express applications. It distributes incoming requests across multiple servers, which improves performance, and if one server fails the others keep taking traffic.
4.2.1 How Load Balancing Distributes Incoming Requests
Load balancers sit between clients and servers, distributing incoming requests across multiple server instances. This helps prevent any single server from becoming overloaded.
4.2.2 Types of Load Balancers:
- Reverse Proxies: Reverse proxies like NGINX or Apache HTTP Server forward client requests to one or more backend servers. Besides load balancing, they can handle SSL termination and caching. They also add a security layer by hiding the details of backend servers from external clients, which reduces the attack surface.
- External Load Balancers: Cloud providers like AWS, Google Cloud, and Azure offer managed load balancing services. These external load balancers can distribute traffic across multiple instances and manage fail-overs. They also integrate with other cloud services.
- Distribution Algorithms:
- Round Robin: Distributes requests in a sequential, cyclic manner.
- Least Connections: Directs traffic to the server with the fewest active connections.
- IP Hash: Uses the client’s IP address to consistently assign requests to the same server, which can be useful for session persistence.
4.2.3 Node.js Clustering for Multi-Core Systems
Node.js applications are single-threaded by nature, meaning they can only use one CPU core. Clustering allows you to take advantage of multi-core systems by creating multiple Node.js processes, each running on a separate core.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isPrimary) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
});
} else {
// Worker processes have an HTTP server.
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello world\n');
}).listen(8000);
}
4.2.4 Popular Load Balancers
- HAProxy: Another widely used load balancer known for its reliability and performance, often used in high-availability setups.
- NGINX: A high-performance load balancer that can handle millions of concurrent connections. Ideal for reverse proxy and web server functionality.
Example Load Balancer Configuration with NGINX:
http {
upstream myapp {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
location / {
proxy_pass http://myapp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
4.2.5 Health Checks and Failover
Load balancers perform health checks to monitor the status of servers. If a server fails, the load balancer reroutes traffic to healthy servers, ensuring continuous availability.
4.2.6 Autoscaling
Combining load balancing with autoscaling allows your application to dynamically adjust the number of running instances based on traffic load. That can reduce the amount of idle capacity you pay for when traffic is low.
4.3 Performance Optimization
Beyond caching and load balancing, optimizing your application’s performance involves writing non-blocking, asynchronous code and managing processes effectively. In this section, we'll briefly cover the importance of asynchronous programming and introduce process management tools like PM2.
4.3.1 Asynchronous Programming in Node.js/Express.js
As we discussed at the beginning of this article Node.js is designed around an event-driven, non-blocking architecture. This design allows your application to handle multiple operations concurrently without waiting for tasks like file I/O or network requests to finish.
Benefits of Asynchronous Programming:
- Non-Blocking I/O: Asynchronous operations prevent the application from being held up by time-consuming tasks like database queries, file I/O, and network requests.
- Improved Responsiveness: By handling tasks asynchronously, the application can remain responsive even under heavy load.
- Efficient Resource Utilization: Asynchronous programming enables better utilization of system resources, allowing the application to handle more concurrent requests.
As you can see in the previous code snippet, the get agent by id contains an async/await code block (4.1.3 (4)), which means that this code is processed asynchronously and won't block the main thread. The Redis call is handled by the event loop (through libuv), and when it completes, the code afterawaitcontinues.
4.3.1 Process Management with PM2
PM2 is a process manager for Node.js applications with process controls, load balancing through clustering, monitoring, and zero-downtime reloads. Benefits of Using PM2:
- Process Management: Start, stop, restart, and monitor multiple Node.js processes with ease.
- Clustering: Utilize multiple CPU cores by running multiple instances of your application, improving performance and fault tolerance.
- Monitoring and Logging: Track metrics such as CPU and memory usage, and view logs for debugging and performance analysis. basic example of using PM2 module
npm install -g pm2
pm2 start app.js
pm2 start app.js -i max
pm2 monit
pm2 logsWhile this section provides an overview of performance optimization techniques such as asynchronous programming and process management with PM2, a deeper dive into these topics will be covered in future articles.
4.4 Testing the endpoints for Ai agents app
Run node index.js in the root folder of the app.
Testing can be done in multiple ways, running the server and open a browser or using curl in the console. I will be using curl.
This will be my dummy agents' data:
//data.js
const agents = [
{
id: 1,
name: 'ChatBot Alpha',
capabilities: ['Natural Language Processing', 'Sentiment Analysis'],
status: 'Active',
createdAt: '2025-01-15T08:00:00Z',
},
{
id: 2,
name: 'VisionBot Beta',
capabilities: ['Image Recognition', 'Object Detection'],
status: 'Inactive',
createdAt: '2025-01-20T10:30:00Z',
},
{
id: 3,
name: 'VoiceBot Gamma',
capabilities: ['Speech Recognition', 'Voice Synthesis'],
status: 'Active',
createdAt: '2025-02-05T14:45:00Z',
},
{
id: 4,
name: 'DataBot Delta',
capabilities: ['Data Analysis', 'Predictive Analytics'],
status: 'Maintenance',
createdAt: '2025-02-10T09:15:00Z',
}]
module.exports = agents;I will add here the list of curl commands with explanation for each of it:
//get all agents
curl -X GET http://localhost:3000/api/agents
//post a new agent
curl -X POST http://localhost:3000/api/agents -H "Content-Type: application/json" -d '{"id": 5, "name": "ChatBot Alpha Beta", "capabilities": ["Natural Language Processing"], "status": "Active", "createdAt": "2025-02-15T08:00:00Z"}'
//get an agent by id (id===1)
curl -X GET http://localhost:3000/api/agents/1
// update an agent by id(id===4)
curl -X PUT http://localhost:3000/api/agents/4 -H "Content-Type: application/json" -d '{"name": "ChatBot Alpha", "capabilities": ["Natural Language Processing", "Sentiment Analysis"], "status": "Active", "createdAt": "2025-01-15T08:00:00Z"}'
//delete an agen by id(id===1)
curl -X DELETE http://localhost:3000/api/agents/1
//instruct an agent to perform a task
curl -X POST http://localhost:3000/api/agents/1/execute -H "Content-Type: application/json" -d '{"task": "Execute Task"}'- retrieving the list of agents(get all agents):
//route handler in the agents.js
router.get('/agents', (req, res, next) => {
res.json(agents);
});you will retrieve the list of agents.
- create a new agent (post a new agent)
router.post('/agents', (req, res, next) => {
const newAgent = req.body;
console.log('New Agent:', newAgent);
agents.push(newAgent)
res.status(201).json(newAgent);
});- get an agent by id:
router.get('/agents/:id', cache, async (req, res, next) => {
const { id } = req.params;
const agent = agents.find(agent => agent.id === +id);
console.log('agent ', agent)
// Store the result in Redis with a TTL (time to live) of 3600 seconds
await redisClient.setEx(`data:${id}`, 3600, JSON.stringify(agent));
res.json(agent);
});In this method as you can see we have redis db for caching so if you try to retrieve the second time the same agent(in no more than 1h) you will get the response from the cached response. Check the console where you have run the project, first time you will see the console.log for the agent and if you run the same command you will see the console from caching :
console.log(`Cache hit for agent ${id}`);The PUT and DELETE handlers below don't clear this cache, so until the TTL expires a GET can still return the old version of an agent that was updated or deleted.
- update an agent In this method we identify the agent that needs to be updated and then we replace it in that list of agent with the new one
router.put('/agents/:id', (req, res, next) => {
const { id } = req.params;
const updatedAgent = req.body;
const index = agents.findIndex(agent => agent.id === +id);
if (index !== -1) {
agents.splice(index, 1, { ...agents[index], ...updatedAgent });
res.status(200).json({ message: `Agent ${id} updated`, agent: agents[index] });
} else {
res.status(404).json({ message: `Agent ${id} not found` });
}
});- delete an agent by id (id===1)
router.delete('/agents/:id', (req, res, next) => {
const { id } = req.params;
const index = agents.findIndex(agent => agent.id === +id);
if (index !== -1) {
agents.splice(index, 1);
res.status(200).json({ message: `Agent ${id} deleted`, agents });
} else {
res.status(404).json({ message: `Agent ${id} not found` });
}
});You will retrieve the list of agents without the deleted one
- Executing a task (you will receive the message "Agent ${id} is executing a task")
router.post('/agents/:id/execute', (req, res, next) => {
const { id } = req.params;
res.send(`Agent ${id} is executing a task`);
});5 Conclusion
The code for the ai-agents app is in the repository below.
https://github.com/florin1000/ai-agents