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

API Reference

Direct API

Server-side API for querying and mutating data directly from Next.js server components, route handlers, and server actions.

The Direct API is the primary way to interact with Nextly data on the server. It calls directly into the service layer -- no HTTP requests, no serialization overhead. Use it in Server Components, Server Actions, API Route Handlers, and anywhere your code runs in a Node.js context.

Getting Started

Import getNextly from @nextlyhq/nextly and call it to get the singleton instance:

app/posts/page.tsx
import { getNextly } from '@nextlyhq/nextly';

const nextly = getNextly();

const posts = await nextly.find({
  collection: 'posts',
  where: { status: { equals: 'published' } },
  sort: '-createdAt',
  limit: 10,
});

You can also use the module-level convenience export, which lazily resolves the singleton on each call:

app/posts/page.tsx
import { nextly } from '@nextlyhq/nextly';

const posts = await nextly.find({
  collection: 'posts',
  limit: 10,
});

Both approaches are equivalent. Use whichever reads better in your code.

Access Control

By default, the Direct API bypasses all access control (overrideAccess: true). This is the expected behavior for trusted server-side code.

To enforce access control, set overrideAccess: false and provide a user context:

const posts = await nextly.find({
  collection: 'posts',
  overrideAccess: false,
  user: { id: 'user-123', role: 'editor' },
});

Collection Operations

find

Find multiple documents in a collection. Returns a paginated response.

const result = await nextly.find({
  collection: 'posts',
  where: { status: { equals: 'published' } },
  sort: '-publishedAt',
  limit: 10,
  page: 1,
  depth: 2,
});

// result.docs          - Post[]
// result.totalDocs     - total matching documents
// result.totalPages    - total pages
// result.hasNextPage   - boolean
// result.hasPrevPage   - boolean
// result.page          - current page number

Parameters:

ParameterTypeDefaultDescription
collectionstringrequiredCollection slug
whereWhereFilterundefinedFilter conditions (see Querying below)
limitnumber10Maximum documents per page
pagenumber1Page number (1-indexed)
sortstringundefinedSort field. Prefix with - for descending
depthnumber0Relationship population depth
selectRecord<string, boolean>undefinedFields to include/exclude
populateRecord<string, boolean | PopulateOptions>undefinedPer-field population control
paginationbooleantrueSet to false to return all documents without pagination metadata
richTextFormat'json' | 'html' | 'both''json'Output format for rich text fields
overrideAccessbooleantrueBypass access control checks

Return type: PaginatedResponse<T>

findByID

Find a single document by ID.

const post = await nextly.findByID({
  collection: 'posts',
  id: 'abc-123',
  depth: 2,
});

Set disableErrors: true to return null instead of throwing when the document is not found:

const post = await nextly.findByID({
  collection: 'posts',
  id: 'maybe-exists',
  disableErrors: true,
});

if (post) {
  console.log(post.title);
}

Parameters:

ParameterTypeDefaultDescription
collectionstringrequiredCollection slug
idstringrequiredDocument ID
depthnumber0Relationship population depth
selectRecord<string, boolean>undefinedFields to include/exclude
disableErrorsbooleanfalseReturn null instead of throwing NotFoundError

Return type: T | null

create

Create a new document.

const post = await nextly.create({
  collection: 'posts',
  data: {
    title: 'Hello World',
    content: 'My first post',
    status: 'draft',
  },
});

console.log('Created:', post.id);

Parameters:

ParameterTypeDefaultDescription
collectionstringrequiredCollection slug
dataRecord<string, unknown>requiredDocument data
draftbooleanfalseCreate as draft
disableVerificationEmailbooleanfalseSkip verification email for auth collections

Return type: T (the created document)

Throws: ValidationError if validation fails.

update

Update an existing document by ID or by where clause.

// Update by ID
const updated = await nextly.update({
  collection: 'posts',
  id: 'abc-123',
  data: { status: 'published', publishedAt: new Date() },
});

// Bulk update by where clause
const updated = await nextly.update({
  collection: 'posts',
  where: { status: { equals: 'draft' } },
  data: { status: 'archived' },
});

Parameters:

ParameterTypeDefaultDescription
collectionstringrequiredCollection slug
idstringundefinedDocument ID (either id or where required)
whereWhereFilterundefinedWhere clause for bulk update
dataRecord<string, unknown>requiredUpdate data

Return type: T (the updated document)

Throws: NotFoundError if no matching documents are found.

delete

Delete documents by ID or by where clause.

// Delete by ID
const result = await nextly.delete({
  collection: 'posts',
  id: 'abc-123',
});

// Bulk delete by where clause
const result = await nextly.delete({
  collection: 'posts',
  where: { status: { equals: 'archived' } },
});

console.log('Deleted IDs:', result.ids);

Parameters:

ParameterTypeDefaultDescription
collectionstringrequiredCollection slug
idstringundefinedDocument ID (either id or where required)
whereWhereFilterundefinedWhere clause for bulk delete

Return type: DeleteResult ({ deleted: boolean, ids: string[] })

count

Count documents matching a query.

const { totalDocs } = await nextly.count({
  collection: 'posts',
  where: { status: { equals: 'published' } },
});

console.log('Published posts:', totalDocs);

Return type: CountResult ({ totalDocs: number })

bulkDelete

Delete multiple documents by an array of IDs. Supports partial success -- some operations may fail while others succeed.

const result = await nextly.bulkDelete({
  collection: 'posts',
  ids: ['post-1', 'post-2', 'post-3'],
});

console.log('Deleted:', result.success);     // string[]
console.log('Failed:', result.failed);       // { id, error }[]

Return type: BulkOperationResult

duplicate

Duplicate an existing document with optional field overrides.

const copy = await nextly.duplicate({
  collection: 'posts',
  id: 'abc-123',
  overrides: { title: 'Copy of Original Post' },
});

Return type: T (the duplicated document)

Singles (formerly Globals) Operations

Singles are single-document entities for site-wide settings like navigation, footer content, or site configuration.

findGlobal

Retrieve a Single document.

const settings = await nextly.findGlobal({
  slug: 'site-settings',
  depth: 1,
});

console.log('Site name:', settings.siteName);

Parameters:

ParameterTypeDefaultDescription
slugstringrequiredSingle/global slug
depthnumber0Relationship population depth
selectRecord<string, boolean>undefinedFields to include/exclude
richTextFormat'json' | 'html' | 'both''json'Rich text output format

Return type: T (the Single document)

updateGlobal

Update a Single document.

const updated = await nextly.updateGlobal({
  slug: 'site-settings',
  data: {
    siteName: 'My Awesome Site',
    maintenanceMode: false,
  },
});

Return type: T (the updated Single document)

findGlobals

Fetch content for all registered Singles.

const result = await nextly.findGlobals();

result.docs.forEach(({ slug, label, data }) => {
  console.log(`${label} (${slug}):`, data);
});

// Filter by source
const codeSingles = await nextly.findGlobals({ source: 'code' });

Return type: SingleListResult ({ docs: GlobalEntry[], totalDocs, limit, offset })

Querying

The where parameter uses a structured query syntax for filtering documents.

Operators

OperatorDescriptionExample
equalsExact match{ status: { equals: 'published' } }
not_equalsNot equal{ status: { not_equals: 'draft' } }
greater_thanGreater than{ price: { greater_than: 100 } }
greater_than_equalGreater than or equal{ price: { greater_than_equal: 100 } }
less_thanLess than{ price: { less_than: 50 } }
less_than_equalLess than or equal{ price: { less_than_equal: 50 } }
likeSQL LIKE pattern match{ title: { like: '%hello%' } }
containsCase-insensitive search{ title: { contains: 'hello' } }
inValue in array{ status: { in: ['draft', 'published'] } }
not_inValue not in array{ status: { not_in: ['archived'] } }
existsField exists (non-null){ image: { exists: true } }

Combining Conditions

Use and and or to combine conditions:

const posts = await nextly.find({
  collection: 'posts',
  where: {
    and: [
      { status: { equals: 'published' } },
      { or: [
        { category: { equals: 'news' } },
        { category: { equals: 'featured' } },
      ]},
      { publishedAt: { less_than: new Date().toISOString() } },
    ],
  },
});

Multiple conditions at the same level are implicitly ANDed:

// These two are equivalent
{ status: { equals: 'published' }, category: { equals: 'news' } }
{ and: [{ status: { equals: 'published' } }, { category: { equals: 'news' } }] }

Sorting

Pass a field name to sort. Prefix with - for descending order:

// Newest first
const posts = await nextly.find({ collection: 'posts', sort: '-createdAt' });

// Alphabetical by title
const posts = await nextly.find({ collection: 'posts', sort: 'title' });

Depth (Relationship Population)

The depth parameter controls how deeply relationship and upload fields are populated:

  • 0 (default) -- return relationship IDs only
  • 1 -- populate direct relationships with their document data
  • 2+ -- populate nested relationships
// No population - author field returns an ID string
const posts = await nextly.find({ collection: 'posts', depth: 0 });
// posts.docs[0].author => "user-abc"

// Populate one level - author field returns the full user object
const posts = await nextly.find({ collection: 'posts', depth: 1 });
// posts.docs[0].author => { id: "user-abc", name: "John", ... }

For fine-grained control, use the populate parameter:

const posts = await nextly.find({
  collection: 'posts',
  depth: 1,
  populate: {
    author: { select: { name: true, email: true } },
    category: false, // Skip population for this field
  },
});

Rich Text Format

Rich text fields store Lexical JSON internally. Use richTextFormat to control the output:

// JSON (default) - returns Lexical editor state
const posts = await nextly.find({ collection: 'posts', richTextFormat: 'json' });

// HTML - returns rendered HTML string
const posts = await nextly.find({ collection: 'posts', richTextFormat: 'html' });
// posts.docs[0].content => "<p>Hello world</p>"

// Both - returns an object with both formats
const posts = await nextly.find({ collection: 'posts', richTextFormat: 'both' });
// posts.docs[0].content => { json: {...}, html: "<p>Hello world</p>" }

TypeScript Support

Run nextly generate:types to generate type-safe collection slugs and document types. Once generated, the Direct API provides full type inference:

// Collection slugs are constrained to valid values
const posts = await nextly.find({ collection: 'posts' });
// posts.docs is typed as Post[]

// Invalid slugs produce compile-time errors
const invalid = await nextly.find({ collection: 'nonexistent' }); // TS error

The generated types augment the GeneratedTypes interface via module declaration:

payload-types.ts (generated)
declare module '@nextlyhq/nextly' {
  export interface GeneratedTypes extends Config {}
}

Error Handling

The Direct API throws typed errors that you can catch and handle:

import { NotFoundError, ValidationError, isNextlyError } from '@nextlyhq/nextly';

try {
  const post = await nextly.findByID({ collection: 'posts', id: 'missing' });
} catch (error) {
  if (error instanceof NotFoundError) {
    // 404 - document not found
    console.log(error.message);
  } else if (error instanceof ValidationError) {
    // 400 - validation failed
    console.log(error.errors); // { fieldName: ['error message'] }
  } else if (isNextlyError(error)) {
    // Other Nextly errors
    console.log(error.code, error.statusCode);
  }
}

Error classes:

ClassCodeHTTP Status
ValidationErrorVALIDATION_ERROR400
UnauthorizedErrorUNAUTHORIZED401
ForbiddenErrorFORBIDDEN403
NotFoundErrorNOT_FOUND404
ConflictErrorCONFLICT409
DuplicateErrorDUPLICATE409
DatabaseErrorDATABASE_ERROR500

Example: Blog Post Listing Page

A complete example using the Direct API in a Next.js Server Component:

app/blog/page.tsx
import { getNextly } from '@nextlyhq/nextly';

export default async function BlogPage({
  searchParams,
}: {
  searchParams: Promise<{ page?: string }>;
}) {
  const { page } = await searchParams;
  const nextly = getNextly();

  const posts = await nextly.find({
    collection: 'posts',
    where: { status: { equals: 'published' } },
    sort: '-publishedAt',
    limit: 12,
    page: Number(page) || 1,
    depth: 1,
    richTextFormat: 'html',
  });

  return (
    <main>
      <h1>Blog</h1>
      <div className="grid grid-cols-3 gap-6">
        {posts.docs.map((post) => (
          <article key={post.id}>
            <h2>{post.title}</h2>
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
          </article>
        ))}
      </div>

      {posts.hasNextPage && (
        <a href={`/blog?page=${posts.nextPage}`}>Next Page</a>
      )}
    </main>
  );
}

Additional Namespaces

The Direct API also provides specialized namespaces for users, media, forms, roles, permissions, components, and API keys. Each namespace follows the same patterns shown above.

NamespaceExampleDescription
nextly.users.find()User managementCRUD operations on users
nextly.media.upload()Media libraryUpload, find, and manage files
nextly.forms.submit()Form submissionsSubmit and query form data
nextly.roles.find()Role managementCRUD operations on RBAC roles
nextly.permissions.find()Permission managementCRUD operations on permissions
nextly.components.find()Component registryManage reusable component definitions
nextly.apiKeys.create()API key managementCreate and manage API keys
nextly.access.check()Access controlCheck user permissions programmatically

Next Steps

  • REST API -- HTTP endpoints for external clients and browser code
  • Client SDK -- TypeScript SDK for browser-side data fetching
  • Collections -- Define the content types that the Direct API operates on
  • Authentication -- RBAC and access control for API operations