Skip to content

Your First Function

This tutorial walks you through creating, deploying, and testing your first function in Crude Functions. You’ll learn the complete workflow from writing code to calling your endpoint.

A simple “Hello World” function that:

  • Responds with JSON
  • Shows the current timestamp
  • Includes request metadata
  • Demonstrates hot-reload capability

Before starting, make sure you have:

  • Crude Functions running at http://localhost:8000
  • Completed the initial setup (created your admin account)
  • Access to the web UI or API

If you haven’t installed Crude Functions yet, see the Getting Started guide.

Every function in Crude Functions is a JavaScript or TypeScript file in the code/ directory. Let’s create our first handler.

Go to the 📁 code management page at http://localhost:8000/web/code and click Upload New File button.

Create a file called hello.ts with the following code:

export default async function (c, ctx) {
return c.json({
message: "Hello from Crude Functions!",
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
});
}

File upload dialog

Every function handler receives exactly two parameters:

ParameterTypePurpose
cHono ContextRequest/response handling (like Express req/res)
ctxFunction ContextRoute metadata, params, query, secrets, request ID

The c parameter lets you:

  • Read request data (c.req.json(), c.req.header(), etc.)
  • Send responses (c.json(), c.text(), c.html(), c.redirect())

The ctx parameter provides:

  • ctx.params - Path parameters (e.g., /users/:id)
  • ctx.query - Query string parameters
  • ctx.requestId - Unique request identifier
  • ctx.authenticatedKeyGroup - API key group (if authenticated)
  • ctx.getSecret() - Access to secrets
  • ctx.route - Route configuration details

See the Handler Context Reference for the complete API.

Now that we have our handler file, we need to register it as a function route. You can do this from the Web UI or via the API.

  1. Navigate to http://localhost:8000/web/functions (the ⚡ tab)
  2. Click the “Create New Function” button
  3. Fill in the form:
FieldValueDescription
Namehello-worldUnique identifier for the function
DescriptionMy first functionHuman-readable description (optional)
Handlerhello.tsPath to handler file - same as in the file management page
Route/helloURL path where function will be accessible
MethodsGETHTTP methods allowed
API Keys(leave empty)No authentication required for now
  1. Click “Create”

Function Creation Dialog

You should see your new function in the functions list with a green “Enabled” status.

Created function listed in table

If you prefer programmatic deployment, you can use the management API:

Terminal window
curl -X POST http://localhost:8000/api/functions \
-H "X-API-Key: your-management-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "hello-world",
"description": "My first function",
"handler": "hello.ts",
"route": "/hello",
"methods": ["GET"]
}'

Your function is now live. Let’s test it.

Terminal window
curl http://localhost:8000/run/hello

You should see a JSON response like:

{
"message": "Hello from Crude Functions!",
"timestamp": "2026-01-16T05:07:24.475Z",
"requestId": "dc791018-0edf-4b15-a8e0-e6d81bd78ff6"
}

Crude Functions automatically captures all console output from your functions.

  1. Go to http://localhost:8000/web/functions
  2. Click the 📝 button on the function entry

The only thing you’ll see are the EXET_START and EXEC_END events because our function doesn’t write anything to the output. Let’s add some logging.

Table with start/end execution logs

Edit your hello.ts file to add some console output:

export default async function (c, ctx) {
console.log(`Hello endpoint called - Request ID: ${ctx.requestId}`);
console.log(`Query parameters:`, ctx.query);
return c.json({
message: "Hello from Crude Functions!",
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
query: ctx.query,
});
}

Save the file. That’s it - no restart needed.

Terminal window
# Call it without query parameters
curl http://localhost:8000/run/hello
# Call it with query parameters
curl "http://localhost:8000/run/hello?name=Alice&role=developer"

Response with query parameters:

{
"message": "Hello from Crude Functions!",
"timestamp": "2026-01-16T05:17:41.122Z",
"requestId": "5455e7d9-2e3d-4267-95cb-691eeb090f16",
"query": {
"name": "Alice",
"role": "developer"
}
}

Go back to the web UI and refresh the Logs tab. You should now see entries like:

Table with logs

Let’s make our function more dynamic by accepting a path parameter.

  1. Go to http://localhost:8000/web/functions
  2. Click ✏️ on your hello-world function
  3. Change the Route Path to /hello/:name
  4. Click “Save”

Edit hello.ts to use the path parameter:

export default async function (c, ctx) {
const name = ctx.params.name || "Guest";
console.log(`Greeting ${name} - Request ID: ${ctx.requestId}`);
return c.json({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
requestId: ctx.requestId,
});
}
Terminal window
curl http://localhost:8000/run/hello/Alice
# Response: {"message": "Hello, Alice!", ...}
curl http://localhost:8000/run/hello/Bob
# Response: {"message": "Hello, Bob!", ...}
curl http://localhost:8000/run/hello/Claude
# Response: {"message": "Hello, Claude!", ...}

Functions can handle multiple HTTP methods. Let’s add POST support.

  1. Edit your function in the web UI
  2. Change HTTP Methods to include both GET and POST
  3. Save

Update the handler to handle both methods differently

Section titled “Update the handler to handle both methods differently”
export default async function (c, ctx) {
const method = c.req.method;
// Handle GET request
if (method === "GET") {
const name = ctx.params.name || "Guest";
console.log(`GET request - Greeting ${name}`);
return c.json({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
});
}
// Handle POST request
if (method === "POST") {
const body = await c.req.json();
console.log(`POST request - Received:`, body);
return c.json({
message: `Hello, ${body.name || "Guest"}!`,
received: body,
timestamp: new Date().toISOString(),
}, 201);
}
// Return error, even though Crude Functions won't let those requests
// into the handler if methods are not allowed in function definition.
return c.json({ error: "Method not allowed" }, 405);
}
Terminal window
# POST with JSON body
curl -X POST http://localhost:8000/run/hello/someone \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "role": "developer"}'

Response:

{
"message": "Hello, Alice!",
"received": {
"name": "Alice",
"role": "developer"
},
"timestamp": "2026-01-16T05:26:53.224Z"
}

Crude Functions tracks execution metrics for every function call.

  1. Go to the functions management page in the Web UI
  2. Click on the 📊 button your hello-world function to view it’s metrics

You’ll see charts showing:

  • Execution time - Average and maximum response times
  • Request count - Number of executions over time

The metrics are aggregated by minute, hour, and day depending on the time range you select.

Metrics view for hello-world function

Congratulations! You’ve created, deployed, and tested your first function. Here’s what to explore next:

Protect your function with API keys:

  1. Go to the API key management page
  2. Create a new key group (e.g., api)
  3. Add an API key to the group
  4. Edit your function select the new group in the Required API Key Groups section
  5. Test with authentication:
Terminal window
# Without key - will fail
curl http://localhost:8000/run/hello/Alice
# With key - will work
curl -H "X-API-Key: your-key-value" http://localhost:8000/run/hello/Alice

Add external dependencies to your function:

import { format } from "npm:date-fns";
import { camelCase } from "npm:lodash-es";
export default async function (c, ctx) {
const name = ctx.params.name || "Guest";
const formatted = camelCase(name);
const timestamp = format(new Date(), "PPpp");
return c.json({
message: `Hello, ${formatted}!`,
timestamp,
});
}

Deno will automatically download and cache the packages on first import.

{
"message": "Hello, someone!",
"timestamp": "Jan 16, 2026, 12:33:30 AM"
}

Store sensitive data like API keys securely:

  1. Go to http://localhost:8000/web/secrets
  2. Add a global secret:
    • Name: GREETING_PREFIX
    • Value: Welcome to Crude Functions
  3. Update your handler:
export default async function (c, ctx) {
const prefix = await ctx.getSecret("GREETING_PREFIX") || "Hello";
const name = ctx.params.name || "Guest";
return c.json({
message: `${prefix}, ${name}!`,
timestamp: new Date().toISOString(),
});
}

Result:

{
"message": "Welcome to Crude Functions, someone!",
"timestamp": "2026-01-16T05:36:15.041Z"
}

Organize your code with shared modules by adding a new lib/formatters.ts file:

export function formatGreeting(name: string): string {
return `Hello, ${name.trim()}!`;
}
export function getTimestamp(): string {
return new Date().toISOString();
}

Then update the hello.ts to use it:

import { formatGreeting, getTimestamp } from "./lib/formatters.ts";
export default async function (c, ctx) {
const name = ctx.params.name || "Guest";
return c.json({
message: formatGreeting(name),
timestamp: getTimestamp(),
});
}