Tools Guide
Overview
Tools are the core of NitroStack - they're functions that AI models can call. In v3.0, tools are defined using the @Tool decorator.
Basic Tool
import { ToolDecorator as Tool, z, ExecutionContext } from 'nitrostack';
export class WeatherTools {
@Tool({
name: 'get_weather',
description: 'Get current weather for a location',
inputSchema: z.object({
city: z.string().describe('City name'),
units: z.enum(['celsius', 'fahrenheit']).optional()
})
})
async getWeather(input: any, context: ExecutionContext) {
context.logger.info(`Fetching weather for ${input.city}`);
// Your logic here
return {
city: input.city,
temperature: 72,
conditions: 'Sunny'
};
}
}
@Tool Decorator
Options
interface ToolOptions {
name: string; // Tool identifier (required)
description: string; // What the tool does (required)
inputSchema?: ZodObject; // Input validation schema
examples?: { // Example request/response
request?: any;
response?: any;
};
}
Example with All Options
@Tool({
name: 'create_user',
description: 'Create a new user account',
inputSchema: z.object({
email: z.string().email().describe('User email'),
name: z.string().min(2).describe('Full name'),
age: z.number().min(13).optional().describe('User age')
}),
examples: {
request: {
email: 'john@example.com',
name: 'John Doe',
age: 25
},
response: {
id: 'user-123',
email: 'john@example.com',
name: 'John Doe',
created_at: '2024-01-15T10:30:00Z'
}
}
})
async createUser(input: any, ctx: ExecutionContext) {
// Implementation
}
Input Validation
Zod Schemas
NitroStack uses Zod for input validation:
import { z } from 'nitrostack';
// String validation
z.string()
z.string().min(3).max(50)
z.string().email()
z.string().url()
z.string().regex(/^[A-Z]+$/)
// Number validation
z.number()
z.number().min(0).max(100)
z.number().int()
z.number().positive()
// Boolean
z.boolean()
// Enum
z.enum(['small', 'medium', 'large'])
// Array
z.array(z.string())
z.array(z.number()).min(1).max(10)
// Object
z.object({
name: z.string(),
age: z.number()
})
// Optional/Required
z.string().optional()
z.string().nullable()
z.string().default('default value')
Complex Schema Example
@Tool({
name: 'create_order',
inputSchema: z.object({
customer: z.object({
name: z.string(),
email: z.string().email()
}),
items: z.array(z.object({
productId: z.string(),
quantity: z.number().int().positive()
})).min(1),
shipping: z.object({
address: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/)
}),
paymentMethod: z.enum(['card', 'paypal', 'apple_pay'])
})
})
async createOrder(input: any, ctx: ExecutionContext) {
// Input is validated before this runs
}
Execution Context
Every tool receives an ExecutionContext:
@Tool({ name: 'my_tool' })
async myTool(input: any, ctx: ExecutionContext) {
// Authentication info
const userId = ctx.auth?.subject;
const token = ctx.auth?.token;
// Logging
ctx.logger.info('Tool started');
ctx.logger.error('Something went wrong');
ctx.logger.debug('Debug info');
// Emit events
ctx.emit('tool.executed', { input, result });
// Tool name
console.log(ctx.toolName); // 'my_tool'
return { success: true };
}
Widgets
Attach UI Components
@Tool({
name: 'get_product',
description: 'Get product details',
inputSchema: z.object({
product_id: z.string()
}),
examples: {
response: {
id: 'prod-1',
name: 'Awesome Product',
price: 99.99
}
}
})
@Widget('product-card') // ← Attach widget
async getProduct(input: any, ctx: ExecutionContext) {
return {
id: input.product_id,
name: 'Awesome Product',
price: 99.99
};
}
The widget at src/widgets/app/product-card/page.tsx will render the response.
Guards
Require Authentication
import { UseGuards } from 'nitrostack';
import { JWTGuard } from '../../guards/jwt.guard.js';
@Tool({ name: 'delete_user' })
@UseGuards(JWTGuard) // ← Requires JWT auth
async deleteUser(input: any, ctx: ExecutionContext) {
const requesterId = ctx.auth?.subject;
// Only authenticated users can call this
}
Multiple Guards
@Tool({ name: 'admin_action' })
@UseGuards(JWTGuard, AdminGuard) // ← Both required
async adminAction(input: any, ctx: ExecutionContext) {
// Only authenticated admins
}
Middleware
Apply Middleware
import { UseMiddleware } from 'nitrostack';
import { LoggingMiddleware } from '../../middleware/logging.middleware.js';
@Tool({ name: 'important_operation' })
@UseMiddleware(LoggingMiddleware)
async importantOperation(input: any, ctx: ExecutionContext) {
// Logging middleware runs before and after
}
Interceptors
Transform Responses
import { UseInterceptors } from 'nitrostack';
import { TransformInterceptor } from '../../interceptors/transform.interceptor.js';
@Tool({ name: 'get_data' })
@UseInterceptors(TransformInterceptor)
async getData(input: any, ctx: ExecutionContext) {
return { value: 42 };
// Interceptor transforms to: { success: true, data: { value: 42 } }
}
Caching
Cache Responses
import { Cache } from 'nitrostack';
@Tool({ name: 'get_product' })
@Cache({
ttl: 300, // 5 minutes
key: (input) => `product:${input.product_id}`
})
async getProduct(input: any, ctx: ExecutionContext) {
// Cached for 5 minutes per product
return await this.productService.findById(input.product_id);
}
Rate Limiting
Limit Request Rate
import { RateLimit } from 'nitrostack';
@Tool({ name: 'send_email' })
@RateLimit({
requests: 10,
window: '1m',
key: (ctx) => ctx.auth?.subject || 'anonymous'
})
async sendEmail(input: any, ctx: ExecutionContext) {
// Max 10 emails per minute per user
}
Dependency Injection
Inject Services
import { Injectable } from 'nitrostack';
@Injectable()
export class ProductService {
async findById(id: string) {
// Database logic
}
}
export class ProductTools {
constructor(private productService: ProductService) {} // ← Injected
@Tool({ name: 'get_product' })
async getProduct(input: any, ctx: ExecutionContext) {
return this.productService.findById(input.product_id);
}
}
Error Handling
Throw Errors
@Tool({ name: 'get_user' })
async getUser(input: any, ctx: ExecutionContext) {
const user = await this.userService.findById(input.user_id);
if (!user) {
throw new Error('User not found');
}
return user;
}
Custom Errors
class UserNotFoundError extends Error {
constructor(userId: string) {
super(`User ${userId} not found`);
this.name = 'UserNotFoundError';
}
}
@Tool({ name: 'get_user' })
async getUser(input: any, ctx: ExecutionContext) {
const user = await this.userService.findById(input.user_id);
if (!user) {
throw new UserNotFoundError(input.user_id);
}
return user;
}
Exception Filters
import { UseFilters } from 'nitrostack';
import { HttpExceptionFilter } from '../../filters/http-exception.filter.js';
@Tool({ name: 'risky_operation' })
@UseFilters(HttpExceptionFilter)
async riskyOperation(input: any, ctx: ExecutionContext) {
// Errors handled by filter
}
Events
Emit Events
@Tool({ name: 'create_order' })
async createOrder(input: any, ctx: ExecutionContext) {
const order = await this.orderService.create(input);
// Emit event
ctx.emit('order.created', {
orderId: order.id,
userId: ctx.auth?.subject,
total: order.total
});
return order;
}
Examples
CRUD Operations
export class UserTools {
constructor(private userService: UserService) {}
@Tool({
name: 'create_user',
description: 'Create a new user',
inputSchema: z.object({
email: z.string().email(),
name: z.string()
})
})
@Widget('user-created')
async createUser(input: any, ctx: ExecutionContext) {
const user = await this.userService.create(input);
ctx.emit('user.created', user);
return user;
}
@Tool({
name: 'get_user',
description: 'Get user by ID',
inputSchema: z.object({
user_id: z.string()
})
})
@Cache({ ttl: 60 })
@Widget('user-card')
async getUser(input: any, ctx: ExecutionContext) {
return this.userService.findById(input.user_id);
}
@Tool({
name: 'update_user',
description: 'Update user details',
inputSchema: z.object({
user_id: z.string(),
name: z.string().optional(),
email: z.string().email().optional()
})
})
@UseGuards(JWTGuard)
async updateUser(input: any, ctx: ExecutionContext) {
return this.userService.update(input.user_id, input);
}
@Tool({
name: 'delete_user',
description: 'Delete a user',
inputSchema: z.object({
user_id: z.string()
})
})
@UseGuards(JWTGuard, AdminGuard)
async deleteUser(input: any, ctx: ExecutionContext) {
await this.userService.delete(input.user_id);
return { success: true };
}
}
Complex Tool
@Tool({
name: 'process_payment',
description: 'Process a payment transaction',
inputSchema: z.object({
order_id: z.string(),
payment_method: z.enum(['card', 'paypal']),
amount: z.number().positive(),
card: z.object({
number: z.string().regex(/^\d{16}$/),
cvv: z.string().regex(/^\d{3,4}$/),
expiry: z.string().regex(/^\d{2}\/\d{2}$/)
}).optional()
}),
examples: {
request: {
order_id: 'order-123',
payment_method: 'card',
amount: 99.99,
card: {
number: '4111111111111111',
cvv: '123',
expiry: '12/25'
}
},
response: {
transaction_id: 'txn-456',
status: 'completed',
amount: 99.99,
timestamp: '2024-01-15T10:30:00Z'
}
}
})
@UseGuards(JWTGuard)
@UseMiddleware(LoggingMiddleware)
@UseInterceptors(TransformInterceptor)
@RateLimit({ requests: 5, window: '1m' })
@Widget('payment-confirmation')
async processPayment(input: any, ctx: ExecutionContext) {
const userId = ctx.auth?.subject;
// Validate order
const order = await this.orderService.findById(input.order_id);
if (!order) {
throw new Error('Order not found');
}
// Process payment
const transaction = await this.paymentService.process({
orderId: input.order_id,
userId,
method: input.payment_method,
amount: input.amount,
card: input.card
});
// Emit events
ctx.emit('payment.processed', transaction);
return transaction;
}
Best Practices
1. Clear Descriptions
// ✅ Good
@Tool({
name: 'search_products',
description: 'Search products by name, category, or price range with pagination'
})
// ❌ Avoid
@Tool({
name: 'search_products',
description: 'Search'
})
2. Use describe() in Schemas
// ✅ Good
inputSchema: z.object({
query: z.string().describe('Search query (product name or SKU)'),
min_price: z.number().optional().describe('Minimum price filter in USD')
})
// ❌ Avoid
inputSchema: z.object({
query: z.string(),
min_price: z.number().optional()
})
3. Provide Examples
// ✅ Good - AI understands better
@Tool({
name: 'get_product',
examples: {
request: { product_id: 'prod-123' },
response: { id: 'prod-123', name: 'Product', price: 99.99 }
}
})
// ❌ Avoid - No examples
@Tool({ name: 'get_product' })
4. Use Services
// ✅ Good - Testable, reusable
export class ProductTools {
constructor(private productService: ProductService) {}
@Tool({ name: 'get_product' })
async getProduct(input: any) {
return this.productService.findById(input.id);
}
}
// ❌ Avoid - Logic in tool
export class ProductTools {
@Tool({ name: 'get_product' })
async getProduct(input: any) {
const db = getDatabase();
return db.query('SELECT * FROM products WHERE id = ?', [input.id]);
}
}
5. Consistent Naming
// ✅ Good - snake_case, verb_noun
'get_product'
'create_user'
'update_order'
'delete_item'
'search_products'
// ❌ Avoid
'getProduct' // camelCase
'Product' // No verb
'DoStuff' // Unclear
Next Steps
Pro Tip: Use nitrostack generate types after defining tools to get TypeScript types for your widgets!