Most frameworks force you to pick a side. Either you define your data schema in code and get version control, type safety, and developer tooling, or you use a visual builder and get accessibility, speed, and independence from the development cycle. Nextly does not ask you to choose. Both approaches are fully supported, produce identical results, and can be used together in the same project.
This post explains how each approach works, what happens under the hood, and when to use which.
The code-first approach
With the code-first approach, you define your collections in TypeScript files using the defineCollection() function. Each collection describes a content type with its fields, validation rules, access control, and admin panel configuration.
Here is a real example of a Products collection:
import {
defineCollection,
text,
number,
richText,
upload,
select,
checkbox,const Products = defineCollection({ slug: "products", admin: { useAsTitle: "name", defaultColumns: ["name", "price", "status"], }, fields: [ text({ name: "name", required: true }), text({ name: "slug", required: true, unique: true }), richText({ name: "description" }), number({ name: "price", required: true, min: 0 }), upload({ name: "image", relationTo: "media" }), select({ name: "status", options: ["draft", "active", "archived"], defaultValue: "draft", }), checkbox({ name: "featured", defaultValue: false }), ], access: { read: () => true, create: ({ req }) => req.user?.role === "admin", update: ({ req }) => req.user?.role === "admin", delete: ({ req }) => req.user?.role === "admin", }, hooks: { beforeChange: [ ({ data }) => { if (data.name && !data.slug) { data.slug = data.name.toLowerCase().replace(/\s+/g, "-"); } return data; }, ], }, });
export default Products;
`
You register this collection in your Nextly config file, and the framework handles the rest: creating database tables, generating API endpoints, building the admin panel interface, and enforcing the access control rules you defined.
Benefits of code-first:
- Version control. Your schema is a TypeScript file in your repository. Every change is tracked in git, reviewable in pull requests, and reversible with a revert.
- Type safety. Your IDE knows the shape of every collection. Autocomplete works for field names, relationship targets, and access control properties. TypeScript catches errors before runtime.
- Testability. You can write automated tests that validate your schema structure, access control rules, and hook behavior.
- Deployment consistency. Schema changes deploy alongside application code. There is no drift between your codebase and your content structure.
- Full feature access. Lifecycle hooks, computed fields, custom validation logic, and conditional field visibility are all configured directly in code.
Code-first is ideal for production applications, teams with CI/CD pipelines, and projects where content structure changes should go through the same review process as application code.
The visual approach
The Schema Builder is a graphical interface in the Nextly admin panel that lets you create and modify collections, singles, and components without writing any code. The builders are organized by type: /admin/collections/builder for collections, /admin/singles/builder for singles (globals), and /admin/components/builder for reusable components.
Here is what the workflow looks like for creating a new collection:
1. Navigate to the collections builder. Go to /admin/collections/builder or click "New Collection" from the collections list in the admin sidebar.
2. Add fields. Click "Add Field" and select a field type from the available options: text, number, rich text, upload, relationship, select, checkbox, date, and more. Each field type has a configuration panel where you set the name, validation rules, default values, and display options.
3. Configure relationships. When adding a relationship field, select the target collection and configure whether it allows single or multiple selections.
4. Set access control. Use the visual access control panel to define who can read, create, update, and delete entries in this collection. Choose from role-based presets or define custom rules.
5. Save and use. Click "Save" and the collection is immediately available. The admin panel shows the new collection in the sidebar, API endpoints are live, and editors can start creating content.
No deployment needed. No code changes. The collection is ready to use the moment you save it.
Benefits of visual:
- Accessibility. Non-technical team members can create and modify content structures without developer involvement.
- Speed. Prototyping a new content type takes minutes instead of a development cycle.
- Immediate feedback. You see the result of your changes instantly in the admin panel.
- No development environment needed. You can modify schema from any browser, including on a production server if your access control allows it. The same visual builder works for collections, singles, and reusable components.
The visual approach is ideal for prototyping, for teams where non-developers need to modify content structures, and for situations where speed of iteration matters more than version control of schema changes.
Same result, different paths
This is the most important thing to understand about the dual approach: both paths produce the same internal representation.
When Nextly starts up, it reads code-defined collections from your TypeScript config files. It also reads visual collections from the database where the Schema Builder stores them. Both sources are merged into a single collection registry. From that point forward, there is no distinction between a code-defined collection and a visual collection.
The same database tables. The same REST and Direct API endpoints. The same admin panel interface for content editors. The same access control enforcement. A content editor creating a blog post cannot tell whether the Posts collection was defined in TypeScript or built in the Schema Builder. They should not have to care.
The export flow
One of the most practical features of the dual approach is the ability to move between visual and code-first at any point.
Start with the Schema Builder for rapid prototyping. Build your collections visually, test them with real content, iterate on the structure until it feels right. This is fast and forgiving, exactly what you want in the early stages of a project.
When you are satisfied with the structure and ready to move toward production, use the export feature. Select a visual collection in the Schema Builder and click "Export to Code." Nextly generates a clean, readable defineCollection() TypeScript file that you can save to your codebase:
// Generated by Schema Builder export
const Events = defineCollection({ slug: "events", admin: { useAsTitle: "name", }, fields: [ text({ name: "name", required: true }), text({ name: "slug", required: true, unique: true }), richText({ name: "description" }), upload({ name: "banner", relationTo: "media" }), relationship({ name: "speakers", relationTo: "users", hasMany: true }), select({ name: "status", options: ["upcoming", "live", "completed"], defaultValue: "upcoming", }), ], });
export default Events;
`
The exported code is not a black box. It is the same defineCollection() syntax you would write by hand. You can modify it, add hooks, refine access control, and commit it to your repository. Once the collection is defined in code, you can remove the visual version from the Schema Builder.
This flow lets non-technical team members contribute to schema design during the prototyping phase, while developers maintain control of the final production schema through code.
When to use which
There is no single right answer. Here are practical guidelines based on common scenarios:
Use code-first when: - You are building a production application with CI/CD - Schema changes should go through pull request review - You need lifecycle hooks, computed fields, or custom validation logic - Your team has established development workflows and deployment pipelines - You want full type safety and IDE support for your schema
Use the Schema Builder when: - You are prototyping a new content type and want to iterate quickly - Non-technical team members need to create or modify content structures - You are exploring what fields and relationships a collection needs before committing to code - You need to make a quick schema change without a deployment cycle
Use both when: - Your team includes developers and non-developers who both need to modify content structures - You want to prototype visually and then export to code for production - Different collections have different requirements: some need strict version control, others benefit from visual flexibility
The dual approach is not about picking one side. It is about having the option to use the right tool for the situation, and changing your mind whenever you need to.
Under the hood
For those interested in the technical details, here is how the dual approach works internally.
Code-defined collections are read from your Nextly config file at startup. The framework parses the defineCollection() calls, validates the field definitions, and registers each collection in the internal registry. Database migrations are generated from the code-defined schema and run as part of your deployment process.
Visual collections are stored in a dedicated system table in your database. When the Schema Builder saves a collection, it writes the field definitions, access control rules, and admin configuration as structured data. At startup, Nextly reads these records and registers them in the same internal registry as code-defined collections. Database tables for visual collections are created and migrated automatically.
The merge process happens once at startup. Code-defined collections take precedence if there is a slug conflict, which prevents a visual collection from accidentally overriding a production schema defined in code. After the merge, the unified registry serves all API requests, admin panel rendering, and access control enforcement.
This architecture means there is no performance difference between code-defined and visual collections. Both are resolved to the same internal representation, and all operations use the same code paths.
Start building with Nextly
Free, open source, and yours to own. No sign-up required.
npx create-nextly-app@latest