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.
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('/thank-you');
res.redirect('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,
contentType: '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('/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, {
contentType: '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