You're reading docs for Nextly Alpha. APIs may change between releases.

API Reference

REST API

HTTP REST API for client-side applications, external services, and any environment where direct server-side access is not available.

Nextly exposes a REST API through a single Next.js catch-all route handler. All collection entries, singles, users, roles, media, and more are accessible via standard HTTP methods. Use this API from browser code, mobile apps, third-party services, or any HTTP client.

Setup

Create a catch-all route handler that delegates to Nextly:

app/api/[[...params]]/route.ts
import { createDynamicHandlers } from '@nextlyhq/nextly';
import nextlyConfig from '../../../nextly.config';

const handlers = createDynamicHandlers({ config: nextlyConfig });
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = handlers;

All API endpoints are served under /api/.

Authentication

The REST API supports two authentication methods:

When a user logs in through the admin panel or the auth endpoints, a session cookie (nextly_cms_session) is set automatically. Requests from the same browser include this cookie.

API Key

Pass an API key in the Authorization header:

curl -H "Authorization: Bearer sk_live_abc123..." \
  https://example.com/api/collections/posts/entries

API keys are created through the Direct API (nextly.apiKeys.create()) or the admin panel.

Collection Entries

Collection entries are the primary data in Nextly. Each collection (e.g., posts, products) has a full set of CRUD endpoints.

List Entries

GET /api/collections/{slug}/entries

Query parameters:

ParameterTypeDefaultDescription
limitnumber10Maximum results per page (max: 500)
pagenumber1Page number (1-indexed)
sortstring--Sort field. Prefix with - for descending
depthnumber0Relationship population depth
where[field][operator]string--Filter conditions (see Query Parameters)

Example:

curl "https://example.com/api/collections/posts/entries?limit=10&sort=-createdAt&where[status][equals]=published"

Response:

{
  "data": {
    "status": 200,
    "success": true,
    "data": {
      "docs": [
        { "id": "abc-123", "title": "Hello World", "status": "published", ... }
      ],
      "totalDocs": 42,
      "limit": 10,
      "totalPages": 5,
      "page": 1,
      "hasNextPage": true,
      "hasPrevPage": false,
      "nextPage": 2,
      "prevPage": null,
      "pagingCounter": 1
    }
  }
}

Get Entry by ID

GET /api/collections/{slug}/entries/{id}

Example:

curl "https://example.com/api/collections/posts/entries/abc-123?depth=2"

Response:

{
  "data": {
    "status": 200,
    "success": true,
    "data": {
      "id": "abc-123",
      "title": "Hello World",
      "status": "published",
      "author": { "id": "user-1", "name": "John Doe" },
      "createdAt": "2025-01-15T10:30:00.000Z",
      "updatedAt": "2025-01-15T10:30:00.000Z"
    }
  }
}

Create Entry

POST /api/collections/{slug}/entries
Content-Type: application/json

Example:

curl -X POST "https://example.com/api/collections/posts/entries" \
  -H "Authorization: Bearer sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "title": "New Post", "content": "Hello!", "status": "draft" }'

Update Entry

PATCH /api/collections/{slug}/entries/{id}
Content-Type: application/json

Send only the fields you want to update:

curl -X PATCH "https://example.com/api/collections/posts/entries/abc-123" \
  -H "Authorization: Bearer sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "status": "published" }'

Delete Entry

DELETE /api/collections/{slug}/entries/{id}
curl -X DELETE "https://example.com/api/collections/posts/entries/abc-123" \
  -H "Authorization: Bearer sk_live_abc123..."

Count Entries

GET /api/collections/{slug}/entries/count

Returns the total number of entries matching a query:

curl "https://example.com/api/collections/posts/entries/count?where[status][equals]=published"

Duplicate Entry

POST /api/collections/{slug}/entries/{id}/duplicate

Creates a copy of an existing entry with new system fields (id, timestamps):

curl -X POST "https://example.com/api/collections/posts/entries/abc-123/duplicate" \
  -H "Authorization: Bearer sk_live_abc123..."

Bulk Delete

POST /api/collections/{slug}/entries/bulk-delete
Content-Type: application/json
curl -X POST "https://example.com/api/collections/posts/entries/bulk-delete" \
  -H "Authorization: Bearer sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["post-1", "post-2", "post-3"] }'

Bulk Update

POST /api/collections/{slug}/entries/bulk-update
Content-Type: application/json

Update multiple entries by IDs:

curl -X POST "https://example.com/api/collections/posts/entries/bulk-update" \
  -H "Authorization: Bearer sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "ids": ["post-1", "post-2"], "data": { "status": "archived" } }'

Bulk Update by Query

PATCH /api/collections/{slug}/entries
Content-Type: application/json

Update all entries matching a where clause:

curl -X PATCH "https://example.com/api/collections/posts/entries" \
  -H "Authorization: Bearer sk_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{ "where": { "status": { "equals": "draft" } }, "data": { "status": "archived" } }'

Singles (formerly Globals)

Singles are single-document entities for site-wide content (settings, navigation, footer).

MethodEndpointDescription
GET/api/singlesList all singles
GET/api/singles/{slug}Get single document content
PATCH/api/singles/{slug}Update single document content
GET/api/singles/{slug}/schemaGet single schema metadata

Example -- fetch site settings:

curl "https://example.com/api/singles/site-settings"

Query Parameters

Where Clauses

Filter results using the where[field][operator]=value format in query strings:

?where[status][equals]=published
?where[price][greater_than]=100
?where[title][contains]=hello
?where[category][in]=news,featured

Supported operators:

OperatorDescriptionQuery string example
equalsExact matchwhere[status][equals]=published
not_equalsNot equalwhere[status][not_equals]=draft
greater_thanGreater thanwhere[price][greater_than]=100
greater_than_equalGreater than or equalwhere[price][greater_than_equal]=100
less_thanLess thanwhere[price][less_than]=50
less_than_equalLess than or equalwhere[price][less_than_equal]=50
likeSQL LIKE patternwhere[title][like]=%hello%
containsCase-insensitive searchwhere[title][contains]=hello
inValue in comma-separated listwhere[status][in]=draft,published
not_inValue not in listwhere[status][not_in]=archived
existsField is non-nullwhere[image][exists]=true

Sorting

Sort by any field. Prefix with - for descending:

?sort=-createdAt       // Newest first
?sort=title            // Alphabetical ascending

Pagination

?limit=20&page=2       // 20 results per page, page 2

Depth

Control relationship population:

?depth=0               // IDs only (default)
?depth=1               // Populate direct relationships
?depth=2               // Populate nested relationships

Users

MethodEndpointDescription
GET/api/usersList all users
POST/api/usersCreate a user
GET/api/users/{id}Get user by ID
PATCH/api/users/{id}Update user
DELETE/api/users/{id}Delete user
GET/api/meGet current authenticated user
PATCH/api/meUpdate current user profile
GET/api/me/permissionsGet current user's permissions

Authentication Endpoints

All auth endpoints are under /api/auth/:

MethodEndpointDescription
POST/api/auth/registerRegister a new user (public)
POST/api/auth/forgot-passwordRequest password reset (public)
POST/api/auth/reset-passwordReset password with token (public)
POST/api/auth/verify-emailVerify email with token (public)
PATCH/api/auth/change-passwordChange password (authenticated)

Session-based login and logout are handled by the Auth.js integration and the session cookie.

Roles and Permissions

Roles

MethodEndpointDescription
GET/api/rolesList all roles
POST/api/rolesCreate a role
GET/api/roles/{id}Get role by ID
PATCH/api/roles/{id}Update role
DELETE/api/roles/{id}Delete role
GET/api/roles/{id}/permissionsList role permissions
POST/api/roles/{id}/permissionsAdd permission to role
PATCH/api/roles/{id}/permissionsReplace all role permissions
DELETE/api/roles/{id}/permissions/{permId}Remove permission from role

Permissions

MethodEndpointDescription
GET/api/permissionsList all permissions
POST/api/permissionsCreate a permission
GET/api/permissions/{id}Get permission by ID
DELETE/api/permissions/{id}Delete permission

Media

Media endpoints handle file uploads and management. Files are stored using the configured storage adapter (local, S3, Vercel Blob).

Refer to the admin panel or Direct API for media upload operations. The REST API handles media through the standard collection entry endpoints for the media collection.

API Keys

MethodEndpointDescription
GET/api/api-keysList API keys for current user
POST/api/api-keysCreate a new API key
GET/api/api-keys/{id}Get API key metadata
PATCH/api/api-keys/{id}Update API key name/description
DELETE/api/api-keys/{id}Revoke an API key

Response Format

All successful responses follow this structure:

{
  "data": {
    "status": 200,
    "success": true,
    "data": { ... }
  }
}

Error Responses

Errors return an appropriate HTTP status code with an error message:

{
  "error": "Resource not found"
}

Common error codes:

StatusMeaning
400Bad request -- invalid input or validation failure
401Unauthorized -- authentication required
403Forbidden -- insufficient permissions
404Not found -- resource does not exist
409Conflict -- duplicate resource or concurrent modification
429Too many requests -- rate limit exceeded
500Internal server error

Security

The REST API includes built-in security features:

  • Rate limiting -- configurable request rate limits per IP
  • CORS -- configurable cross-origin request handling
  • Security headers -- standard security headers applied to all responses
  • Super-admin protection -- prevents non-super-admins from assigning the super_admin role

These are configured in your nextly.config.ts via the security and rateLimit options. See Configuration for details.

Example: Fetch and Display Posts

Client-side fetch
async function fetchPosts(page = 1) {
  const params = new URLSearchParams({
    limit: '10',
    page: String(page),
    sort: '-createdAt',
    'where[status][equals]': 'published',
    depth: '1',
  });

  const response = await fetch(`/api/collections/posts/entries?${params}`);
  const json = await response.json();

  return json.data.data; // PaginatedResponse
}

Next Steps

  • Direct API -- Server-side API for direct database access (no HTTP overhead)
  • Client SDK -- TypeScript SDK that wraps this REST API for browser use
  • Authentication -- RBAC, API keys, and access control
  • Collections -- Define the content types that the REST API serves