Building a Fiori To-Do App and Deploying it to Synology NAS
This is a full walkthrough of building a real SAP Fiori application from scratch — a To-Do app with a REST backend — and deploying it as a Docker container on a Synology NAS. No SAP backend required. No BTP account. Just Node.js, OpenUI5, and a NAS you already have.
By the end you will have:
- A dark-themed Fiori app with routing, data binding, a detail page, and due dates
- A json-server REST backend that persists data to a file
- Two Docker images running on your Synology, accessible from your local network
Prerequisites
- Node.js 22+ and npm — install via NodeSource
- @ui5/cli and @sap/ux-ui5-tooling — installed below
- Docker —
curl -fsSL https://get.docker.com | sudo sh - A Synology NAS with Container Manager installed
1. Project structure
Create a folder and initialise it:
mkdir fiori-todo-app && cd fiori-todo-app
The final layout will be:
fiori-todo-app/
├── db.json # json-server database
├── package.json
├── ui5.yaml # UI5 Tooling config
├── Dockerfile # App image (nginx)
├── Dockerfile.api # API image (json-server)
├── nginx.conf
├── docker-compose.yml # For building locally
├── docker-compose.synology.yml
├── start-api.sh
└── webapp/
├── index.html
├── Component.js
├── manifest.json
├── view/
│ ├── App.view.xml
│ ├── Main.view.xml
│ └── Detail.view.xml
├── controller/
│ ├── App.controller.js
│ ├── Main.controller.js
│ └── Detail.controller.js
├── i18n/
│ └── i18n.properties
└── css/
└── style.css
2. package.json and tooling
The scripts section wires up the dev experience: npm run dev starts both the UI5 dev server and json-server in parallel via concurrently.
Install:
npm install
3. ui5.yaml
This file tells the UI5 toolchain which libraries to use, how to serve the app locally, and how to proxy API requests so you don't hit CORS issues in development.
The backend entry is key: any request your app makes to /todos gets forwarded by the dev server to http://localhost:3001 (json-server). In production, nginx handles the same proxy.
4. The database
json-server turns this JSON file into a full REST API automatically. Save it in the project root. Every mutation (POST, PATCH, DELETE) is written back to this file immediately.
5. webapp/index.html
The entry point. The data-sap-ui-bootstrap script tag loads the UI5 core and wires up the component.
The data-sap-ui-theme="sap_horizon_dark" gives the modern dark Horizon theme. sapUiSizeCompact makes controls smaller and denser — standard for desktop Fiori apps.
6. webapp/manifest.json
The app descriptor. Every Fiori app has one. It declares routing, models, dependencies, and CSS. Routing is the most important part here — it maps URL hash patterns to views.
Note on
controlId: The router injects views into the<App>NavContainer. The value"app"works correctly when usingfiori runorui5 servebecause the tooling controls how component IDs are generated. It does not work with a plain Python or static file server — if you ever switch back to those, you will get a white screen.
7. webapp/Component.js
The UIComponent is the application's entry point. It creates the central JSONModel, loads todos from the API, and starts the router.
The API URL is /todos — a relative path. The dev server proxies it to json-server; nginx does the same in Docker.
8. The root view and shell controller
App.view.xml is the navigation container. The router injects Main and Detail pages into <App id="app"> at runtime.
9. i18n and CSS
All user-visible text lives in one file. Bind it in views with {i18n>key}.
10. Main view
This is where most of the Fiori concepts live — template binding, expression binding, a formatter, filter tabs, and the delete button.
Key patterns to notice:
items="{/todos}"— template binding, creates one row per object in the arrayclass="{= ${done} ? 'todoItemDone' : 'todoItem'}"— expression binding, switches CSS class at runtime{path: 'dueDate', formatter: '.formatDate'}— formatter function converts"2026-06-15"→"Jun 15, 2026"visible="{= !!${dueDate} }"— expression binding hides the due date row when empty
11. Main controller
Every mutation calls the API immediately. Optimistic updates keep the UI responsive — the model changes first, then the request fires in the background.
webapp/controller/Main.controller.js →
12. Detail view and controller
bindElement (called in the controller) binds the entire view to one specific todo. Every binding — {title}, {done}, {dueDate} — resolves against that object without any path prefix.
13. Run it locally
Start both servers with one command:
npm run dev
Open http://localhost:8080. The app loads, fetches todos from json-server, and you can add, complete, delete, and filter tasks. Every change is written back to db.json automatically.
The fiori-tools-proxy middleware forwards /todos requests from the browser to http://localhost:3001, so there are no CORS issues.
14. Docker — the app image
The Dockerfile uses a two-stage build. Stage one uses Node.js to build the UI5 app (ui5 build -a bundles everything including the UI5 framework files). Stage two copies only the compiled output into a lightweight nginx image.
nginx does two things: serves the static UI5 app, and proxies /todos to the json-server container. The hostname api is the Docker Compose service name — Docker's internal DNS resolves it automatically.
15. Docker — the API image
json-server needs --host 0.0.0.0 to listen on all interfaces inside the container, not just localhost. The entrypoint script seeds db.json on the first run if the mounted volume is empty.
16. Docker Compose
docker-compose.yml is for building and running locally. docker-compose.synology.yml references pre-imported images by name — no build context — for use in Synology Container Manager.
17. Build the images and export
Run these on your development machine (Docker must be installed):
# Build both images
docker build -t fiori-todo-app:latest -f Dockerfile .
docker build -t fiori-todo-api:latest -f Dockerfile.api .
# Export to compressed tar archives
docker save fiori-todo-app:latest | gzip > fiori-todo-app.tar.gz
docker save fiori-todo-api:latest | gzip > fiori-todo-api.tar.gz
Each archive will be around 60 MB.
18. Deploy to Synology
- Copy both
.tar.gzfiles to your Synology (via File Station orscp) - Open Container Manager → Image → Add → Import from file
- Import
fiori-todo-app.tar.gz→ appears asfiori-todo-app:latest - Import
fiori-todo-api.tar.gz→ appears asfiori-todo-api:latest - Go to Container Manager → Project → Create
- Paste the contents of
docker-compose.synology.yml - Click Deploy
The app will be available at http://your-synology-ip:8080.
The todo-data Docker volume persists db.json across container restarts and image updates. Your tasks survive everything except explicitly deleting the volume.
Key concepts summary
| Concept | Where it appears |
|---|---|
JSONModel + two-way binding |
Component.js creates the model; {done} on CheckBox syncs automatically |
| Template binding | items="{/todos}" — one row per array entry |
| Expression binding | class="{= ${done} ? 'todoItemDone' : 'todoItem'}" |
| Formatter function | formatDate() converts "2026-06-15" → "Jun 15, 2026" |
sap.ui.model.Filter |
_applyFilter() filters the list binding without touching model data |
Routing + navTo |
Defined in manifest.json; Main.controller.js calls router.navTo("detail", { id }) |
bindElement |
Detail.controller.js binds the whole view to one todo object |
DatePicker valueFormat / displayFormat |
Storage format vs display format are kept separate |
fetch with optimistic updates |
Model updates immediately; API call fires in background |
| nginx reverse proxy | Single port for app + API; Docker service name used as hostname |
| Multi-stage Docker build | Node.js builds the app; nginx serves it — final image has no Node.js |