packages/website/content/articles/what-can-you-build-in-50-lines-of-yaml.md
Most web frameworks tend to make you write a lot of code before anything actually works. A React form needs state hooks, event handlers, validation logic, JSX markup, and probably a CSS file. A dashboard needs a charting library, data fetching, data filtering, and loading states. A data table needs... you get the idea.
Sometimes I don't want to obsess over syntax and convoluted setups. I just want to describe what I need and have it work.
Lowdefy is an open-source, config-first web framework. You write YAML, and it gives you a front-to-back web app. It's built on Next.js with 70+ UI components, database connectors, and built-in auth, without the need to write any React or JavaScript. When you hit a wall, there's support for custom plugins and a _js operator that lets you drop into JavaScript when you need to.
I put together three examples to showcase it, each built in 50 lines of YAML.
Let's start simple. A contact form with three fields, required validation, a database save, and a reset. Normally you'd need a React component, a validation library, an API route, and a database client.
In Lowdefy:
id: contact-us
type: Card
layout:
contentGutter: 16
properties:
title: Contact Us
style:
width: 500
margin: 40px auto
requests:
- id: save_message
connectionId: contact-messages
type: MongoDBInsertOne
payload:
form:
_state: form
properties:
doc:
_payload: form
blocks:
- id: form.name
type: TextInput
required: true
properties:
title: Your Name
- id: form.email
type: TextInput
required: true
properties:
title: Email
- id: form.message
type: TextArea
required: true
properties:
title: Message
placeholder: How can we help?
- id: submit
type: Button
properties:
title: Send Message
block: true
events:
onClick:
- id: validate
type: Validate
- id: save
type: Request
params: save_message
- id: reset
type: Reset
50 lines. That gives you:
The events.onClick array defines the entire flow. Actions run sequentially: validate first, then save to the database, then reset. If validation fails, the chain stops. No if statements, no try-catch. Just declare the flow.
Notice how the input ids are prefixed with form.. That nests them under a form object in the page state. The payload passes that object to the server via _state: form, and the request accesses it with _payload: form to save as the document. Clean separation between client and server. The connectionId references a database connection defined in lowdefy.yaml, which we'll show at the end.
Dashboards are usually where the complexity explodes, between charting libraries, data transformations, and responsive containers. Here's a bar chart with a toggle, a target line, and a legend, all powered by Apache ECharts under the hood:
id: monthly-revenue
type: Card
properties:
title: Monthly Revenue
style:
maxWidth: 700
margin: 40px auto
events:
onMount:
- id: set_default
type: SetState
params:
chart_type: bar
blocks:
- id: chart_type
type: ButtonSelector
properties:
label:
disabled: true
options:
- label: Bar Chart
value: bar
- label: Line Chart
value: line
- id: revenue_chart
type: EChart
properties:
height: 400
option:
tooltip:
trigger: axis
legend:
data: [Revenue, Target]
xAxis:
type: category
data: [Jan, Feb, Mar, Apr, May, Jun]
yAxis:
type: value
series:
- name: Revenue
type:
_state: chart_type
data: [12000, 19200, 15400, 22800, 18600, 25100]
- name: Target
type: line
data: [15000, 15000, 18000, 18000, 20000, 20000]
lineStyle:
type: dashed
48 lines. Two series, a legend, tooltips, and an interactive toggle. Hover over any bar to see the tooltip, and click a series name in the legend to toggle it on or off.
The chart type is bound to the selector's state through this line:
type:
_state: chart_type
_state is a Lowdefy operator, a function that runs inline in your config. When the user clicks "Line Chart", the selector's value changes to line, and the chart re-renders as a line chart. No event handler. No manual state wiring. The binding is declarative.
Lowdefy has 50+ operators like this. _if for conditionals, _sum for math, _string for text manipulation, _array for list operations. You can combine them to handle most logic without needing to write any JavaScript.
To pull data from a real API or database, replace the hardcoded
dataarray with_request: my_sales_dataand add a MongoDB or REST request to the page. The chart updates automatically. We show exactly how in the next example.
The first example wrote to a database, and this one reads from it. A searchable company directory that queries MongoDB in real time, with a sortable table. Usually that means a backend API, a frontend data table library, and a search implementation:
id: company-search
type: Card
layout:
contentGutter: 16
style:
margin: 50
requests:
- id: get_companies
connectionId: companies
type: MongoDBAggregation
payload:
search:
_state: search
properties:
pipeline:
- $match:
trading_name:
$regex:
_if_none:
- _payload: search
- ''
$options: i
events:
onMount:
- id: fetch
type: Request
params: get_companies
blocks:
- id: search
type: TextInput
properties:
title: Search Company
events:
onChange:
- id: refetch
type: Request
params: get_companies
- id: companies_table
type: AgGridBalham
properties:
overlayNoRowsTemplate: Loading...
rowData:
_request: get_companies
columnDefs:
- headerName: Company
field: trading_name
sortable: true
- headerName: Description
field: description
flex: 1
50 lines. That includes:
MongoDBAggregation runs a native MongoDB pipeline. The $match stage uses _if_none to handle the initial null state. When the search is empty, the regex matches everything. You could add $lookup, $group, or $project stages just as easily.payload, then accessed with _payload in the pipeline.onChange, which re-runs the aggregation. The table updates instantly._request: get_companies pipes the aggregation results directly into the table's rowData. When the request re-runs, the table re-renders.The connections and pages are all defined in your app's lowdefy.yaml config file:
lowdefy: 4.7.2
name: Lowdefy starter
connections:
- id: contact-messages
type: MongoDBCollection
properties:
collection: contact-messages
databaseUri:
_secret: MONGODB_URI
write: true
- id: companies
type: MongoDBCollection
properties:
collection: companies
databaseUri:
_secret: MONGODB_URI
pages:
- _ref: contact-us.yaml
- _ref: monthly-revenue.yaml
- _ref: company-search.yaml
Secrets are managed through environment variables, never hardcoded in config.
Three examples, ~150 lines of YAML total:
In React, the equivalent would easily be 300-500 lines across component files, hook logic, API routes, validation, and CSS. Here, it's all in the config.
For perspective, we run production apps on Lowdefy, including a manufacturing ERP with 390+ pages, a multi-app CRM with complex multi-step workflows, and a support desk serving 400+ client companies. All built with the same framework and same YAML-first approach, just scaled up.
Want to try it? Create an empty directory and run:
pnpx lowdefy@4 init && pnpx lowdefy@4 dev
Drop any of the examples above into a .yaml file in your app directory, reference it in lowdefy.yaml, and you're running.
These 50-line examples barely scratch the surface. Lowdefy ships with:
If you're curious, the examples above are a good place to start. Honestly, my favourite part about Lowdefy is not having to deal with braces and semicolons.
Lowdefy is open source and free to use. Star the repo on GitHub if this was useful.