Routes
Routes allow you to create custom HTTP endpoints for webhooks, APIs, and form handlers. They enable your Nimbu site to respond to HTTP requests with custom server-side logic.
Routes allow you to create custom HTTP endpoints for webhooks, APIs, and form handlers. They enable your Nimbu site to respond to HTTP requests with custom server-side logic.
What are Routes?
Routes are HTTP endpoints that:
- Handle incoming web requests (GET, POST, PUT, PATCH, DELETE)
- Process webhooks from external services
- Create custom APIs for your frontend or mobile apps
- Handle form submissions with custom logic
- Serve dynamic content or JSON responses
Defining Routes
HTTP Method Shortcuts
Use method-specific shortcuts for common HTTP verbs:
Nimbu.Cloud.get('/path', handler);
Nimbu.Cloud.post('/path', handler);
Nimbu.Cloud.put('/path', handler);
Nimbu.Cloud.patch('/path', handler);
Nimbu.Cloud.delete('/path', handler);Generic Route Method
Or use the generic route() method:
Nimbu.Cloud.route('GET', '/path', handler);
Nimbu.Cloud.route('POST', '/path', handler);Path Patterns
Routes support different path patterns:
Static Paths
Nimbu.Cloud.get('/api/products', (req, res) => {
// Matches exactly /api/products
});Path Parameters
Use :param syntax for dynamic segments:
Nimbu.Cloud.get('/api/products/:id', (req, res) => {
const productId = req.params.id;
// Matches /api/products/123, /api/products/abc, etc.
});
Nimbu.Cloud.get('/users/:userId/orders/:orderId', (req, res) => {
const { userId, orderId } = req.params;
// Matches /users/123/orders/456
});Wildcard Paths
Use * for catch-all routes:
Nimbu.Cloud.get('/docs/*path', (req, res) => {
const fullPath = req.params.path;
// Matches /docs/anything/here
});Request Object
The request object provides information about the HTTP request:
| Property | Description | Example |
|---|---|---|
req.params | URL parameters, query string, and body data | req.params.id |
req.path | Request path | /api/products |
req.headers | HTTP headers | req.headers['content-type'] |
req.body | Raw request body | Raw string data |
req.host | Request hostname | yoursite.nimbu.io |
req.locale | Site locale | nl or en |
req.customer | Authenticated customer | req.customer.get('email') |
req.session | Session data | req.session.get('cart') |
Accessing Parameters
Parameters come from three sources (merged into req.params):
- URL parameters - From the path pattern (
:id) - Query string - From
?key=value - Request body - From POST/PUT/PATCH payloads
// Route: /api/products/:id?detailed=true
// Body: {"quantity": 5}
Nimbu.Cloud.get('/api/products/:id', (req, res) => {
req.params.id; // From URL path
req.params.detailed; // From query string
req.params.quantity; // From request body
});Response Methods
Use the response object to send different types of responses:
JSON Response
Send JSON data:
res.json({ status: 'success', data: results });HTML Response
Send HTML content:
res.html('<h1>Hello World</h1>');Redirect
Redirect to another URL:
res.redirect_to('/thank-you');
res.redirect_to('https://example.com');Render Template
Render a Liquid template:
res.render('template_name', {
variable1: 'value1',
variable2: 'value2'
});Success/Error
Simple success or error responses:
res.success();
res.error('Something went wrong');
res.error(404, 'Not found');Send File/Data
Send file data:
res.send(fileData, {
status: 200,
type: 'application/pdf',
filename: 'invoice.pdf'
});Real-World Examples
Example 1: Form Handler
Handle form submissions (real example from vito-gstic-2018 project):
// Handle newsletter subscription form
Nimbu.Cloud.post('/forms/newsletter', async (req, res) => {
console.log('Processing newsletter subscription');
const entry = new Nimbu.Object('newsletter');
entry.set('name', req.params.name);
entry.set('email', req.params.email);
entry.set('organization', req.params.organization);
entry.set('country', req.params.country);
await entry.save();
res.redirect_to('/thank-you');
});
// Generic form handler for any channel
Nimbu.Cloud.post('/forms/:channel', async (req, res) => {
const channel = req.params.channel;
console.log(`Processing form for ${channel}`);
const entry = new Nimbu.Object(channel);
// Set all form fields
Object.keys(req.params).forEach(key => {
if (!['channel', 'path'].includes(key) && req.params[key] !== '') {
entry.set(`${channel}_${key}`, req.params[key]);
}
});
await entry.save();
res.json({ success: true, id: entry.id });
});Example 2: Newsletter Unsubscribe
Unsubscribe link from newsletter (real example from theme-hr-gids):
Nimbu.Cloud.get('/nieuwsbrief-uitschrijven/:channel/:number', async (req, res) => {
const { number, channel } = req.params;
const query = new Nimbu.Query('customers');
query.equalTo('number', number);
const customer = await query.first();
if (customer) {
customer.set(`newsletter_${channel}_off`, true);
await customer.save();
res.html(`
<h1>Uitgeschreven</h1>
<p>Je bent succesvol uitgeschreven van de nieuwsbrief.</p>
`);
} else {
res.html('<h1>Klant niet gevonden</h1>');
}
});Example 3: Product API Endpoint
Create a JSON API for products:
// List products
Nimbu.Cloud.get('/api/products', async (req, res) => {
const { category, limit = 20, page = 1 } = req.params;
const query = new Nimbu.Query('products');
query.equalTo('active', true);
if (category) {
query.equalTo('category', category);
}
query.limit(parseInt(limit));
query.skip((page - 1) * limit);
const products = await query.collection().fetch();
const total = await query.count();
res.json({
products: products.map(p => ({
id: p.id,
name: p.get('name'),
price: p.get('price'),
image: p.get('image_url')
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: total,
pages: Math.ceil(total / limit)
}
});
});
// Get single product
Nimbu.Cloud.get('/api/products/:id', async (req, res) => {
try {
const product = await new Nimbu.Query('products').get(req.params.id);
res.json({
id: product.id,
name: product.get('name'),
description: product.get('description'),
price: product.get('price'),
images: product.get('images'),
inStock: product.get('stock') > 0
});
} catch (error) {
res.error(404, 'Product not found');
}
});Example 4: Webhook Handler
Handle incoming webhooks from external services:
const HTTP = require('http');
Nimbu.Cloud.post('/webhooks/stripe', async (req, res) => {
console.log('Stripe webhook received');
const event = JSON.parse(req.body);
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
// Find the order
const query = new Nimbu.Query('orders');
query.equalTo('payment_intent_id', paymentIntent.id);
const order = await query.first();
if (order) {
order.set('payment_status', 'paid');
order.set('paid_at', new Date());
await order.save();
console.log('Order marked as paid:', order.id);
}
}
res.json({ received: true });
});Example 5: File Download
Generate and download files:
const Csv = require('csv');
Nimbu.Cloud.get('/export/customers', async (req, res) => {
// Check authentication
if (!req.customer || !req.customer.get('is_admin')) {
res.error(403, 'Forbidden');
return;
}
// Fetch customers
const query = new Nimbu.Query('customers');
const customers = await query.collection().fetch();
// Generate CSV
const data = [
['Name', 'Email', 'Created At'],
...customers.map(c => [
c.get('name'),
c.get('email'),
c.get('created_at')
])
];
const csv = Csv.to_csv(data);
res.send(csv, {
type: 'text/csv',
filename: 'customers.csv'
});
});Example 6: Dynamic Content
Serve dynamic HTML content:
Nimbu.Cloud.get('/newsletter/preview/:id', async (req, res) => {
const newsletter = await new Nimbu.Query('newsletters').get(req.params.id);
res.render('newsletter_template', {
title: newsletter.get('title'),
content: newsletter.get('content'),
articles: newsletter.get('articles')
});
});Example 7: Search Endpoint
Create a search API:
Nimbu.Cloud.get('/api/search', async (req, res) => {
const { q, type = 'products' } = req.params;
if (!q || q.length < 2) {
res.error(400, 'Search query must be at least 2 characters');
return;
}
const query = new Nimbu.Query(type);
query.matches('name', q, 'i'); // Case-insensitive search
const results = await query.collection().fetch();
res.json({
query: q,
type: type,
results: results.map(item => ({
id: item.id,
name: item.get('name'),
url: `/products/${item.get('slug')}`
}))
});
});Working with Sessions
Routes have access to session data:
// Store data in session
Nimbu.Cloud.post('/cart/add', (req, res) => {
const cart = req.session.get('cart') || [];
cart.push(req.params.product_id);
req.session.set('cart', cart);
res.json({ itemsInCart: cart.length });
});
// Read from session
Nimbu.Cloud.get('/cart', (req, res) => {
const cart = req.session.get('cart') || [];
res.json({ items: cart });
});Authentication
Check if a customer is authenticated:
Nimbu.Cloud.get('/account/profile', (req, res) => {
if (!req.customer) {
res.error(401, 'Please log in');
return;
}
res.json({
name: req.customer.get('name'),
email: req.customer.get('email')
});
});Error Handling
Always handle errors gracefully:
Nimbu.Cloud.post('/api/order', async (req, res) => {
try {
// Validate input
if (!req.params.items || req.params.items.length === 0) {
res.error(400, 'Order must contain at least one item');
return;
}
// Create order
const order = new Nimbu.Object('orders');
order.set('items', req.params.items);
order.set('customer', req.customer);
await order.save();
res.json({ orderId: order.id });
} catch (error) {
console.error('Order creation failed:', error.message);
res.error(500, 'Failed to create order');
}
});Best Practices
1. Use Appropriate HTTP Methods
// Good: Correct HTTP methods
Nimbu.Cloud.get('/api/products', listProducts); // Read
Nimbu.Cloud.post('/api/products', createProduct); // Create
Nimbu.Cloud.put('/api/products/:id', updateProduct); // Update
Nimbu.Cloud.delete('/api/products/:id', deleteProduct); // Delete
// Avoid: Using GET for everything
Nimbu.Cloud.get('/create-product', createProduct); // Wrong!2. Validate Input
Nimbu.Cloud.post('/api/contact', async (req, res) => {
// Validate required fields
if (!req.params.email) {
res.error(400, 'Email is required');
return;
}
if (!req.params.message) {
res.error(400, 'Message is required');
return;
}
// Process contact form...
});3. Return Proper Status Codes
// 200 - Success
res.json({ status: 'ok' });
// 201 - Created
res.json({ id: newId }, { status: 201 });
// 400 - Bad Request
res.error(400, 'Invalid input');
// 401 - Unauthorized
res.error(401, 'Authentication required');
// 404 - Not Found
res.error(404, 'Resource not found');
// 500 - Server Error
res.error(500, 'Internal server error');4. Secure Sensitive Endpoints
Nimbu.Cloud.post('/admin/delete-all', (req, res) => {
// Check authentication
if (!req.customer) {
res.error(401, 'Authentication required');
return;
}
// Check permissions
if (!req.customer.get('is_admin')) {
res.error(403, 'Admin access required');
return;
}
// Perform sensitive operation...
});5. Log Important Actions
Nimbu.Cloud.post('/webhooks/payment', async (req, res) => {
console.log('[Webhook] Payment webhook received');
console.log('[Webhook] Event type:', req.params.type);
try {
await processPayment(req.params);
console.log('[Webhook] Payment processed successfully');
res.json({ received: true });
} catch (error) {
console.error('[Webhook] Payment processing failed:', error.message);
res.error(500, 'Processing failed');
}
});6. Handle CORS for APIs
Nimbu.Cloud.route('OPTIONS', '/api/*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.success();
});
Nimbu.Cloud.get('/api/products', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*');
// Return data...
});Common Patterns
Rate Limiting
const rateLimits = {};
Nimbu.Cloud.post('/api/contact', (req, res) => {
const ip = req.headers['x-forwarded-for'];
const now = Date.now();
if (rateLimits[ip] && (now - rateLimits[ip]) < 60000) {
res.error(429, 'Too many requests. Please try again later.');
return;
}
rateLimits[ip] = now;
// Process request...
});Pagination
Nimbu.Cloud.get('/api/articles', async (req, res) => {
const page = parseInt(req.params.page) || 1;
const limit = parseInt(req.params.limit) || 20;
const query = new Nimbu.Query('articles');
query.limit(limit);
query.skip((page - 1) * limit);
const articles = await query.collection().fetch();
const total = await query.count();
res.json({
articles: articles,
pagination: {
page: page,
limit: limit,
total: total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});Accessing Routes
Routes are accessible at:
https://yoursite.nimbu.io/cloud/routes/your-pathFor example:
- Route:
Nimbu.Cloud.get('/api/products', ...) - URL:
https://yoursite.nimbu.io/cloud/routes/api/products
Testing Routes
Using curl
# GET request
curl https://yoursite.nimbu.io/cloud/routes/api/products
# POST request
curl -X POST https://yoursite.nimbu.io/cloud/routes/forms/contact \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"[email protected]"}'From JavaScript
fetch('https://yoursite.nimbu.io/cloud/routes/api/products')
.then(res => res.json())
.then(data => console.log(data));Next Steps
- Learn about Cloud Functions - Callable server-side functions
- Explore Callbacks - React to data changes
- Review Background Jobs - Schedule automated tasks
- Check Extensions - Add custom admin actions
Cloud Functions
Cloud Functions let you define custom server-side logic that can be called from your themes, mobile apps, or external services. They provide a secure way to execute business logic…
Extensions
Extensions allow you to add custom actions and functionality to the Nimbu admin interface. Use them to create bulk operations, custom workflows, or specialized tools for your team.