Serverless Functions

Extend your nself backend with serverless functions for custom business logic, webhooks, scheduled tasks, and API integrations.

Overview

nself provides multiple ways to add custom server-side logic through serverless functions, Hasura Actions, Event Triggers, and background workers. All options integrate seamlessly with your GraphQL API and database.

Function Types

Hasura Actions

Extend your GraphQL API with custom business logic:

# Actions allow you to:
# - Add custom mutations and queries
# - Integrate with external APIs
# - Implement complex business logic
# - Handle authentication workflows
# - Process payments

# Example: Custom user registration action
mutation RegisterUser($input: RegisterUserInput!) {
  registerUser(input: $input) {
    id
    email
    success
    message
  }
}

Event Triggers

React to database changes with automatic function execution:

# Triggers fire on:
# - INSERT operations
# - UPDATE operations  
# - DELETE operations

# Example: Send welcome email on user signup
# Trigger: users table INSERT
# Function: send-welcome-email

# Payload includes:
# - Event type (INSERT/UPDATE/DELETE)
# - Old and new row data
# - Session variables
# - Headers

Scheduled Events

Run functions on a schedule (cron jobs):

# Examples:
# - Daily data cleanup: "0 2 * * *"
# - Hourly report generation: "0 * * * *"
# - Weekly backups: "0 0 * * 0"
# - Monthly billing: "0 0 1 * *"

# Cron expressions support:
# - Minutes (0-59)
# - Hours (0-23)
# - Day of month (1-31)
# - Month (1-12)
# - Day of week (0-7, 0 or 7 is Sunday)

Function Implementation

NestJS Functions

Use TypeScript with the NestJS framework for structured, enterprise-ready functions:

Enable NestJS Service

# In .env.local:
NESTJS_ENABLED=true
NESTJS_PORT=3001

# Rebuild and start
nself build && nself up

Example NestJS Action

// functions/nestjs/src/actions/user.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('actions')
export class UserController {
  constructor(private userService: UserService) {}

  @Post('register-user')
  async registerUser(@Body() input: any) {
    const { email, password, name } = input.input;
    
    try {
      // Custom business logic
      const user = await this.userService.create({
        email,
        password,
        name
      });
      
      // Send welcome email
      await this.userService.sendWelcomeEmail(user);
      
      return {
        id: user.id,
        email: user.email,
        success: true,
        message: 'User registered successfully'
      };
    } catch (error) {
      return {
        success: false,
        message: error.message
      };
    }
  }
}

Node.js Functions

Simple functions using plain Node.js:

// functions/node/register-user.js
const bcrypt = require('bcryptjs');
const { sendEmail } = require('./utils/email');

module.exports = async (req, res) => {
  const { email, password, name } = req.body.input;
  
  try {
    // Hash password
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // Insert user into database
    const query = `
      INSERT INTO users (email, password_hash, name, email_verified)
      VALUES ($1, $2, $3, false)
      RETURNING id, email
    `;
    
    const result = await db.query(query, [email, hashedPassword, name]);
    const user = result.rows[0];
    
    // Send welcome email
    await sendEmail({
      to: email,
      subject: 'Welcome to our platform!',
      template: 'welcome',
      data: { name }
    });
    
    res.json({
      id: user.id,
      email: user.email,
      success: true,
      message: 'User registered successfully'
    });
    
  } catch (error) {
    res.status(400).json({
      success: false,
      message: error.message
    });
  }
};

Python Functions

Use Python for data processing, ML, and AI workloads:

# functions/python/sentiment_analysis.py
from flask import Flask, request, jsonify
import pandas as pd
from textblob import TextBlob

app = Flask(__name__)

@app.route('/actions/analyze-sentiment', methods=['POST'])
def analyze_sentiment():
    data = request.get_json()
    text = data['input']['text']
    
    # Perform sentiment analysis
    blob = TextBlob(text)
    sentiment = blob.sentiment
    
    return jsonify({
        'polarity': sentiment.polarity,
        'subjectivity': sentiment.subjectivity,
        'classification': 'positive' if sentiment.polarity > 0 else 'negative' if sentiment.polarity < 0 else 'neutral'
    })

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3002)

Go Functions

High-performance functions with Go:

// functions/go/image-processor/main.go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "image"
    "image/jpeg"
    _ "image/png"
)

type ResizeRequest struct {
    Input struct {
        ImageURL string `json:"image_url"`
        Width    int    `json:"width"`
        Height   int    `json:"height"`
    } `json:"input"`
}

func resizeImageHandler(w http.ResponseWriter, r *http.Request) {
    var req ResizeRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Download and resize image
    resizedURL, err := processImage(req.Input.ImageURL, req.Input.Width, req.Input.Height)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    response := map[string]interface{}{
        "resized_url": resizedURL,
        "success": true,
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

func main() {
    http.HandleFunc("/actions/resize-image", resizeImageHandler)
    fmt.Println("Go function server starting on :3003")
    http.ListenAndServe(":3003", nil)
}

Function Configuration

Environment Variables

Functions inherit environment variables from your nself configuration:

# Available to functions:
HASURA_GRAPHQL_ENDPOINT=http://hasura:8080/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET=your-admin-secret
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DATABASE=myproject
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your-password

# Custom function variables:
SENDGRID_API_KEY=your-sendgrid-key
STRIPE_SECRET_KEY=your-stripe-key
JWT_SECRET=your-jwt-secret

Function URLs

Functions are accessible at predictable endpoints:

# NestJS functions
http://localhost:3001/actions/[action-name]
http://localhost:3001/webhooks/[webhook-name]

# Node.js functions  
http://localhost:3000/functions/[function-name]

# Python functions
http://localhost:3002/actions/[action-name]

# Go functions
http://localhost:3003/actions/[action-name]

Hasura Integration

Creating Actions

Connect functions to your GraphQL API:

1. Define Action in Hasura Console

# Action Definition:
type Mutation {
  registerUser(input: RegisterUserInput!): RegisterUserOutput
}

# Input Type:
input RegisterUserInput {
  email: String!
  password: String!
  name: String!
}

# Output Type:
type RegisterUserOutput {
  id: UUID
  email: String
  success: Boolean!
  message: String!
}

2. Configure Handler URL

# Handler URL: http://nestjs:3001/actions/register-user
# Headers:
# - Content-Type: application/json
# - X-Hasura-User-Id: {{X-Hasura-User-Id}}
# - Authorization: Bearer {{SESSION_VARIABLES['x-hasura-auth-token']}}

Event Triggers

# Example: User welcome email trigger
# Table: users
# Operations: INSERT
# Webhook URL: http://nestjs:3001/webhooks/user-created
# Headers: 
# - Content-Type: application/json
# - X-Hasura-Admin-Secret: {{ADMIN_SECRET}}

Background Jobs

BullMQ Workers

Process long-running tasks asynchronously:

// Enable BullMQ in .env.local:
BULLMQ_ENABLED=true
REDIS_URL=redis://redis:6379

// Queue job from action:
const emailQueue = new Queue('email', { connection: redis });

await emailQueue.add('welcome-email', {
  userId: user.id,
  email: user.email,
  name: user.name
});

// Process in worker:
const worker = new Worker('email', async job => {
  const { userId, email, name } = job.data;
  await sendWelcomeEmail(email, name);
}, { connection: redis });

Scheduled Jobs

# Create scheduled event in Hasura:
# Name: daily-cleanup
# Webhook: http://nestjs:3001/scheduled/cleanup
# Schedule: 0 2 * * * (2 AM daily)
# Payload: {"task": "cleanup_old_data"}

// Handler:
@Post('scheduled/cleanup')
async dailyCleanup(@Body() payload: any) {
  // Delete old temporary files
  await this.fileService.cleanupTempFiles();
  
  // Archive old logs
  await this.logService.archiveOldLogs();
  
  // Update analytics
  await this.analyticsService.updateDailyStats();
  
  return { success: true };
}

Testing Functions

Local Testing

# Test functions locally
curl -X POST http://localhost:3001/actions/register-user \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "email": "test@example.com",
      "password": "password123",
      "name": "Test User"
    }
  }'

# Test with Hasura variables
curl -X POST http://localhost:3001/actions/get-user-profile \
  -H "Content-Type: application/json" \
  -H "X-Hasura-User-Id: 123" \
  -d '{"input": {}}'

Unit Testing

// NestJS testing
import { Test } from '@nestjs/testing';
import { UserController } from './user.controller';

describe('UserController', () => {
  let controller: UserController;
  
  beforeEach(async () => {
    const module = await Test.createTestingModule({
      controllers: [UserController],
    }).compile();
    
    controller = module.get<UserController>(UserController);
  });
  
  it('should register user successfully', async () => {
    const input = {
      email: 'test@example.com',
      password: 'password123',
      name: 'Test User'
    };
    
    const result = await controller.registerUser({ input });
    expect(result.success).toBe(true);
    expect(result.email).toBe(input.email);
  });
});

Deployment

Production Configuration

# Production .env settings
NODE_ENV=production
NESTJS_ENABLED=true
PYTHON_ENABLED=true

# Function-specific settings
MAX_REQUEST_SIZE=10mb
TIMEOUT=30s
MEMORY_LIMIT=512mb

# Security
CORS_ORIGINS=https://yourdomain.com
RATE_LIMIT_ENABLED=true
RATE_LIMIT_MAX=100

Monitoring

# Function monitoring
nself logs nestjs -f
nself logs python-worker -f

# Performance metrics
nself resources | grep functions

# Error tracking
nself logs --grep "error" --since "1h"

Best Practices

  • Stateless Functions: Keep functions stateless for easy scaling
  • Error Handling: Implement comprehensive error handling
  • Input Validation: Validate all inputs on function entry
  • Timeout Handling: Set appropriate timeouts for external calls
  • Logging: Log function execution for debugging and monitoring
  • Security: Validate authentication and authorization
  • Performance: Monitor function execution time and memory usage
  • Testing: Write comprehensive unit and integration tests

Common Use Cases

  • Authentication: Custom signup/login workflows
  • Payments: Stripe/PayPal integration
  • Email/SMS: Transactional messaging
  • File Processing: Image/video manipulation
  • Data Processing: Analytics and reporting
  • External APIs: Third-party service integration
  • Webhooks: Receiving data from external services
  • Scheduled Tasks: Maintenance and cleanup jobs

Next Steps