The API folder Last updated: 23. Oct 2026

Create your custom JSON WEB API by placing JS files that will run server side in the #/API folder.

The #/API folder

Any .js file placed in this folder runs on the server side at /API/<name> (without .js), and its return value is converted to JSON. Other files, such as .hash, are not reachable at /API/….

This means if you put any .js files in this folder they will run server side. Unlike any other folder where they will just be served raw to the front end.

An API response is application/json unless the script sets another type with docly.setMime(...). (Outside the API folder, a .hash file gets its type from the extension before .hash: style.css.hash is text/css, Sitemap.xml.hash is XML; a name without one is text/html. docly.setMime overrides it.)

Subfolders work as routes, and the .js extension is added for you: /API/hello runs #/API/hello.js, and /API/admin/users runs #/API/admin/users.js.

Code in #/API/ always runs as plain JavaScript. The HashJS compatibility mode (hashjs.compatibilityMode in #/site.json) never applies here, whatever the setting. See Start new sites in strict JavaScript mode.

Your first API endpoint

Below is a minimal endpoint you can drop into your workspace right now. Create the file #/API/hello.js:

// #/API/hello.js
export default () => {
    return { message: "Hello from Docly API" };
}

The endpoint is now available at /API/hello on your site and returns the object above as JSON. Docly serializes the return value for you — no Content-Type or JSON.stringify boilerplate.

To accept input, name the fields you expect as parameters: each parameter is filled with the posted form field (or querystring parameter) of the same name. Do not name a parameter form — it would receive a field called "form", not the form object. The function below answers with a greeting and validates that the caller supplied a name:

// #/API/greet.js
export default (name) => {
    if (!name) throw new Error("Missing name parameter");
    return { message: "Hello, " + name };
}

From a hash page or any browser-side script, call it with fetch():

<script>
    fetch('/API/greet', {
        method: 'POST',
        body: new URLSearchParams({ name: 'Anna' })
    })
        .then(r => r.json())
        .then(d => console.log(d.message));
</script>

Why keep dynamic logic in #/API/ rather than in a .hash-template? Hash files are cached aggressively by Docly, while API functions run on every request. See Hash files are cached for the full reasoning and the failure mode it avoids.

API files are standard JavaScript

API files always run in API mode: standard JavaScript without Docly's older template syntax, whatever #/site.json says.

The legacy template helpers (add, cint, where, the global trim(text) and so on) exist only with the docly. prefix, for example docly.cint(value). Standard methods such as text.trim() and array.push() work as normal. See Legacy template helpers.

Reading a property of null or undefined throws a TypeError, as in standard JavaScript. Use optional chaining (obj?.field) where a value may be missing.

#…# expressions and the <!--#include--> / <!--#master--> directives are not processed in API files. Share code between files with import { name } from "#/Lib/file.js" — see import.

Responses and errors

Whatever your function returns is serialized to JSON and sent with 200 OK. You control that shape.

A thrown Error is different. Docly catches it and replaces your shape with its own envelope — none of your own fields survive:

// return { message: "Hello, Anna" }
200   {"message":"Hello, Anna"}

// throw new Error("Missing name parameter")
403   {"ResultCode":0,"Message":"Missing name parameter"}

Two things catch callers out: the field is Message in PascalCase, like the rest of the Docly document format — and the status is 403, not 400. Client code that has only ever seen the success shape tends to look for error or message, find neither, and fall back to a generic "something went wrong" — silently discarding the message the server actually sent.

ResultCode is the machine-readable companion to Message, so the caller can branch on the kind of failure instead of matching on the text. A plain throw new Error(...) leaves it at 0. To set your own, use docly.assert(description, resultCode) — it throws the same envelope with the code you supply, and defaults to 1 when you omit it:

// #/API/greet.js
export default (name) => {
    if (!name) docly.assert("Missing name parameter", 101);
    return { message: "Hello, " + name };
}

// 403   {"ResultCode":101,"Message":"Missing name parameter"}

The HTTP status stays 403 whichever code you set — ResultCode lives in the body only. Reach for docly.assert when the caller genuinely needs to distinguish failures programmatically; otherwise throw new Error(...) is the simpler and recommended form.

A fetch() caller therefore has to handle both shapes:

const r = await fetch('/API/greet', {
    method: 'POST',
    body: new URLSearchParams({ name: '' })
});
const d = await r.json();
const msg = d.Message || d.message || d.error;   // thrown error vs. your own shape

To keep your own field names and the 403, throw a plain object instead of an Error. Docly sends the object as the response body, unchanged:

// #/API/greet.js
export default (name) => {
    if (!name) throw { ok: false, error: "Missing name parameter", code: "validation" };
    return { ok: true, message: "Hello, " + name };
}

// 403   {"ok":false,"error":"Missing name parameter","code":"validation"}

If you would rather have one shape everywhere, return the error instead of throwing it. The response is then 200 with your own object, and you decide the field names:

// #/API/greet.js
export default (name) => {
    if (!name) return { ok: false, error: "Missing name parameter", code: "validation" };
    return { ok: true, message: "Hello, " + name };
}

The trade-off: throw gives a real 4xx status that logging and monitoring can see, while return always yields 200 and moves the outcome into the body. A common split is to throw for genuine server faults and return for expected user errors, so the status code still means something.

For how input reaches your function in the first place, see Reading query and form fields in API calls.

API functions (read and write)

Because the API functions are not cached on the server an extra set of functions are available when running JS from the API folder. These are here to make it possible to create dynamic web applications that your static frontend (always cached) can access data dynamically from.

The extra functions available include:

From the API you can read values for querystring from the "query" object and "form" object for posted form values.

See functions marked with API only in the JavaScript Reference.

Security

Docly provides a comprehensive security framework for your API endpoints, including automated threat detection, progressive blocking mechanisms, and built-in protection against common attack vectors. Understanding how authentication and authorization work in Docly is essential for building secure applications.

Docly handles user authentication through its built-in login system. When you publish a folder or website with 'Login Required', Docly ensures that only invited users with valid accounts can access your application. However, you are responsible for implementing authorization logic in your API endpoints to verify that authenticated users have the appropriate permissions for specific operations.

When you flag suspicious activity with docly.flagActivity(), Docly counts the flags per IP address and blocks the address when there are too many:

  1. An IP address is blocked when it collects enough flags within three days. How many it takes depends on the country the address is in: 100 for trusted, 25 for neutral and 5 for untrusted countries. Repeats of the same event count once.

  2. The first block lasts 30 days, the second 90 days and every later block 365 days. Unblocking resets this, so the next block starts at 30 days again.

  3. A blocked visitor can unblock the address in steps: the first time by passing a reCAPTCHA, the second time with a reCAPTCHA plus a confirmation link sent by email (valid for 30 minutes), and after that only by manual review.

Activity Flagging: docly.flagActivity() records at most one flag per API call, and returns true when the address is blocked (or just became blocked). Use it for activities such as:

  • Failed login attempts

  • Invalid API requests

  • Suspicious access patterns

  • Unauthorized access attempts

Path Security: All paths in Docly are absolute paths within each webapp. This design eliminates path traversal and injection vulnerabilities, as there is no way to escape the webapp's directory structure.

Authentication vs Authorization: Docly handles authentication - verifying that a user with an invitation to your application is logged in before they can access it. However, you must implement authorization in your API endpoints - checking whether an authenticated user has permission to perform specific actions or access particular data. Unless you're running a public application or implementing a custom login experience, you should maintain your own internal table of users and access rights to control what authenticated users can do within your application.

Use POST for anything that changes data. See State-changing API endpoints must require POST.

Caching

API functions are always executed on each call and not cached, unlike .hash files in other folders, which are cached until the underlying data changes and then regenerated on demand on the next request.

Request limits

A request body may be at most 30 MB (30,000,000 bytes). A form post may have at most 1024 fields, field names of at most 2048 characters and values of at most 4 MiB.

A request over a limit is rejected before your code runs: 413 when the body is too large, 400 when a form limit is exceeded.

For all available functions inside an API endpoint, see the JavaScript Reference. Particularly useful inside an API: Filesystem functions, Network functions, Mail functions, Authentication.