8.6 KiB
//[doc-seo]
{
"Description": "Define JavaScript-backed custom REST endpoints in the ABP Low-Code System without writing custom .NET controllers."
}
Custom Endpoints
Preview: Custom endpoint descriptors and scripting helpers are part of the preview Low-Code System. Names, validation rules, and runtime behavior may change before general availability.
Use generated page CRUD APIs for normal dynamic entity pages. Custom endpoints are an advanced option for exposing small model-owned REST APIs that do not map to standard list, get, create, update, delete, export, file, or attachment operations.
Custom endpoints are defined in JSON descriptor files or through the Low-Code Designer. Each endpoint executes server-side JavaScript and is registered as an ASP.NET Core route.
Defining Endpoints
{
"endpoints": [
{
"name": "GetCampaignStats",
"route": "/api/custom/campaigns/stats",
"method": "GET",
"description": "Get campaign statistics",
"requireAuthentication": true,
"requiredPermissions": ["Acme.Campaigns"],
"javascript": "var count = await db.count('Acme.Campaigns.Campaign');\nreturn ok({ totalCampaigns: count });"
}
]
}
Endpoint Descriptor
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | Required | Unique endpoint identifier |
route |
string | Required | URL route pattern; must start with / and can contain {parameters} |
method |
string | GET |
GET, POST, PUT, DELETE, or PATCH |
javascript |
string | Required | JavaScript handler code |
description |
string | null | Optional designer/documentation text |
requireAuthentication |
bool | true |
Whether the caller must be authenticated |
requiredPermissions |
string[] | null | Permission names required to call the endpoint |
Permission checks require an authorized user even when requireAuthentication is set to false. Keep endpoints authenticated by default and use requireAuthentication: false only for intentionally public APIs without requiredPermissions.
Route and Request Data
Use {paramName} syntax for route parameters. Endpoint scripts can read request data through globals:
| Variable | Description |
|---|---|
request |
Full request object |
route |
Route values, for example route.id |
params |
Alias for route |
query |
Query string values, for example query.q |
body |
Parsed request body |
headers |
Selected safe request headers |
{
"name": "GetCampaignById",
"route": "/api/custom/campaigns/{id}",
"method": "GET",
"javascript": "var campaign = await db.get('Acme.Campaigns.Campaign', route.id);\nif (!campaign) { return notFound('Campaign not found'); }\nreturn ok({ id: campaign.Id, name: campaign.Name });"
}
For non-GET requests, body is parsed when a body is present and remains subject to the configured request size limit. The headers object intentionally contains only selected request headers such as Content-Type, Accept, Accept-Language, and X-Requested-With.
Response Helpers
Endpoint scripts can return plain data, an endpoint response object, or one of the response helpers.
| Function | HTTP status | Response kind |
|---|---|---|
ok(data, headers?) |
200 | JSON |
okText(text, contentType?) |
200 | Text |
okBinary(base64Data, contentType?) |
200 | Binary from base64 |
created(data, headers?) |
201 | JSON |
noContent() |
204 | Empty |
badRequest(message) |
400 | JSON error |
unauthorized(message) |
401 | JSON error |
forbidden(message) |
403 | JSON error |
notFound(message) |
404 | JSON error |
error(message) |
500 | JSON error |
For custom status codes or response metadata, return an object with response fields:
return {
statusCode: 202,
kind: 'json',
data: { accepted: true },
headers: { 'x-trace-id': guid() }
};
Response kinds are:
| Kind | Description |
|---|---|
json |
JSON serialization, default |
text |
Plain text |
binaryBase64 |
Base64-encoded binary payload |
Script Services
Custom endpoint scripts use the same common Scripting API services as other low-code scripts:
| Service | Example |
|---|---|
db |
Query or mutate dynamic entities |
user / currentUser |
Read current user information |
tenant / currentTenant |
Read tenant information |
auth / authorization |
Check permissions |
settings, features, config |
Read allowed settings, features, and app configuration |
http |
Call allowed outbound HTTP services |
email |
Send or queue email |
events, jobs |
Publish distributed events or enqueue background jobs |
files, images, attachments |
Work with low-code files and record attachments |
log, logWarning, logError |
Write logs |
Examples
Statistics
{
"name": "GetCampaignStats",
"route": "/api/custom/campaigns/stats",
"method": "GET",
"requireAuthentication": true,
"javascript": "var campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar total = await campaignQuery.count();\nvar active = await campaignQuery.where(c => c.Status === 1).count();\nreturn ok({ total: total, active: active });"
}
Search with Query Parameters
{
"name": "SearchCampaigns",
"route": "/api/custom/campaigns/search",
"method": "GET",
"requireAuthentication": true,
"javascript": "var q = query.q || '';\nvar campaignQuery = await db.query('Acme.Campaigns.Campaign');\nvar rows = await campaignQuery\n .where(c => c.Name.toLowerCase().includes(q.toLowerCase()))\n .take(10)\n .toList();\nreturn ok(rows.map(c => ({ id: c.Id, name: c.Name })));"
}
Create with Validation
{
"name": "CreateCampaignDraft",
"route": "/api/custom/campaigns/draft",
"method": "POST",
"requireAuthentication": true,
"requiredPermissions": ["Acme.Campaigns.Create"],
"javascript": "if (!body.name) { return badRequest('Name is required.'); }\nvar record = await db.insert('Acme.Campaigns.Campaign', { Name: body.name, Status: 0 });\nreturn created({ id: record.Id, name: record.Name });"
}
Testing Endpoint Scripts
The Low-Code Designer endpoint editor includes Test JavaScript. Use it to run the current editor content without saving it.
The dry-run request editor lets you provide:
- HTTP method
- Request path
- Route values
- Query values
- Headers
- Body JSON
- Outbound HTTP mocks
Dry-run execution evaluates the endpoint descriptor, request context, script, authentication metadata, and required permissions against the current user. It returns the same response shape that a real endpoint execution would return.
Side effects are captured instead of being sent to external systems:
| Operation | Dry-run behavior |
|---|---|
| Database writes | Rolled back |
| Email send or queue | Captured under Captured Side Effects |
| Event publish | Captured under Captured Side Effects |
| Background job enqueue | Captured under Captured Side Effects |
| Outbound HTTP | Matched against HTTP mocks |
| File, image, and attachment operations | Captured without persisting files |
If a script calls the http helper and no mock matches the method and URL, the result contains a mock miss instead of sending a real HTTP request.
Response Policy
Dynamic endpoint responses are validated by LowCode:Scripting:EndpointResponse.
{
"LowCode": {
"Scripting": {
"EndpointResponse": {
"MaxBodyBytes": 1048576,
"AllowedContentTypes": [
"application/json",
"text/plain",
"application/octet-stream"
],
"BlockedHeaders": [
"Set-Cookie",
"Content-Length",
"Content-Type"
]
}
}
}
}
Default blocked headers also include hop-by-hop headers such as Connection, Transfer-Encoding, and Upgrade.
Content-Type is blocked as a custom response header. Choose the response kind or contentType field instead of setting a raw Content-Type header from script.
Security Notes
- Prefer authenticated endpoints with explicit
requiredPermissions. - Treat endpoints with
requireAuthentication: falseand norequiredPermissionsas public API surface. - Keep endpoint scripts small and focused.
- Validate route, query, and body input before using it.
- Use
take()for list queries. - Use the configured HTTP, email, file, and response limits for untrusted integrations.