Skip to content

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:

js
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:

js
Nimbu.Cloud.route('GET', '/path', handler);
Nimbu.Cloud.route('POST', '/path', handler);

Path Patterns

Routes support different path patterns:

Static Paths

js
Nimbu.Cloud.get('/api/products', (req, res) => {
  // Matches exactly /api/products
});

Path Parameters

Use :param syntax for dynamic segments:

js
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:

js
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:

PropertyDescriptionExample
req.paramsURL parameters, query string, and body datareq.params.id
req.pathRequest path/api/products
req.headersHTTP headersreq.headers['content-type']
req.bodyRaw request bodyRaw string data
req.hostRequest hostnameyoursite.nimbu.io
req.localeSite localenl or en
req.customerAuthenticated customerreq.customer.get('email')
req.sessionSession datareq.session.get('cart')

Accessing Parameters

Parameters come from three sources (merged into req.params):

  1. URL parameters - From the path pattern (:id)
  2. Query string - From ?key=value
  3. Request body - From POST/PUT/PATCH payloads
js
// 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:

js
res.json({ status: 'success', data: results });

HTML Response

Send HTML content:

js
res.html('<h1>Hello World</h1>');

Redirect

Redirect to another URL:

js
res.redirect('/thank-you');
res.redirect('https://example.com');

Render Template

Render a Liquid template:

js
res.render('template_name', {
  variable1: 'value1',
  variable2: 'value2'
});

Success/Error

Simple success or error responses:

js
res.success();
res.error('Something went wrong');
res.error(404, 'Not found');

Send File/Data

Send file data:

js
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):

js
// 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):

js
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:

js
// 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:

js
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:

js
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:

js
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:

js
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:

js
// 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:

js
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:

js
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

js
// 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

js
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

js
// 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

js
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

js
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

js
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

js
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

js
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-path

For example:

  • Route: Nimbu.Cloud.get('/api/products', ...)
  • URL: https://yoursite.nimbu.io/cloud/routes/api/products

Testing Routes

Using curl

bash
# 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

js
fetch('https://yoursite.nimbu.io/cloud/routes/api/products')
  .then(res => res.json())
  .then(data => console.log(data));

Next Steps