Skip to main content

Adding a backend to SVAR Gantt in Nuxt

In Part 1 we built a Nuxt app with Gantt driven by in-memory data. This guide shows you how to connect that chart to SQLite database. By the end it loads from SQLite, writes every change through REST, and supports creating, deleting, moving, and linking tasks. The full code is on the backend branch of the same repository.

1. Install SQLite

We use SQLite here: a single file on disk, with no service to run and no container to manage.

npm install better-sqlite3
npm install -D @types/better-sqlite3

2. Define the schema

Gantt data splits into two related types: tasks and the links connecting them. The schema needs two tables to match. Create server/utils/db.ts:

CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT DEFAULT '',
start TEXT,
end TEXT,
duration INTEGER,
progress INTEGER DEFAULT 0,
type TEXT,
parent INTEGER DEFAULT 0,
orderId INTEGER DEFAULT 0
);

CREATE TABLE links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source INTEGER NOT NULL,
target INTEGER NOT NULL,
type TEXT NOT NULL
);

A few things to note:

  • Dates live as TEXT. There's no date column type in SQLite, so we keep ISO strings like "2026-04-02", and the data provider turns them into Date objects client-side.
  • Pair start with duration, or with end: store whichever the task uses. Summary rows can skip both. The chart derives their span from the children beneath them.
  • parent shapes the hierarchy, orderId fixes the order. Moving a task rewrites the orderId values among its siblings so the arrangement holds after a reload.

Insert sample tasks and links if the tables are empty, so the chart has data to display.

3. Create the API routes

In Nuxt, each server endpoint is just a file under server/api/. Its filename and method suffix determine the route:

server/api/tasks/
index.get.ts → GET /api/tasks
index.post.ts → POST /api/tasks
[taskId].get.ts → GET /api/tasks/:taskId
[taskId].put.ts → PUT /api/tasks/:taskId
[taskId].delete.ts → DELETE /api/tasks/:taskId

server/api/links/
index.get.ts → GET /api/links
index.post.ts → POST /api/links
[id].get.ts → GET /api/links/:id
[id].put.ts → PUT /api/links/:id
[id].delete.ts → DELETE /api/links/:id

The GET handler returns all tasks:

export default defineEventHandler(() => {
return getAllTasks();
});

You don't need to import anything. Nuxt auto-imports everything in server/utils/, so your database helpers are already in scope inside a route.

The full REST surface:

MethodEndpointPurpose
GET/tasksList all tasks
POST/tasksCreate a task
PUT/tasks/{id}Update or move a task
DELETE/tasks/{id}Remove a task
GET/linksList all links
POST/linksCreate a link
PUT/links/{id}Update a link
DELETE/links/{id}Remove a link

The client depends on four contracts:

  • POST replies with { "id": 123 } so the client can replace its temporary ID with the permanent one.
  • DELETE replies with {}.
  • Creating a task carries { task, mode, target }, where mode is "child", "after", or "before" and target is the task it's positioned against.
  • Moving a task rides on the same PUT route, distinguished by an operation flag. One handler covers both edits and moves:
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, "taskId"));
const body = await readBody(event);

if (body.operation === "move") {
return { id: Number(moveTask(id, body.target, body.mode)) };
}
return { id: Number(updateTask(id, body)) };
});

Open /api/tasks in your browser to verify the setup. It returns a JSON array of tasks.

4. Install the data provider

Instead of writing fetch calls by hand, use SVAR data provider:

npm install @svar-ui/gantt-data-provider

5. Load data from the server

In GanttChart.vue, replace the hardcoded arrays with a RestDataProvider pointed at the API base. A single call loads both tasks and links:

import { ref, onMounted } from "vue";
import { RestDataProvider } from "@svar-ui/gantt-data-provider";

const server = new RestDataProvider("/api");

const tasks = ref<any[]>([]);
const links = ref<any[]>([]);

onMounted(() => {
server.getData().then((data) => {
tasks.value = data.tasks;
links.value = data.links;
});
});

getData() requests GET /api/tasks and GET /api/links, converts the date strings into Date objects, and returns { tasks, links } ready to bind. scales stay in the component: they're display settings, not stored data, so the database never sees them.

Reload the page. The chart now fills from the database. Edits don't persist yet.

6. Connect saves to the chart

One line connects the provider to the chart action pipeline:

function init(ganttApi: IApi) {
api.value = ganttApi;
ganttApi.setNext(server);
}

After you call setNext, every chart action flows through the provider. The Gantt emits actions such as add-task, update-task, delete-task, move-task, add-link, and delete-link, and the provider translates each into the right POST, PUT, or DELETE. It also reconciles the temporary IDs with the ones the server hands back.

That's the whole integration. It needs no manual fetch calls or event handlers.

Drag a bar, edit dates in the sidebar, or draw a link between two tasks. Then reload the page. Every change is still there.

7. Add the toolbar

Part 1 relied on double-click for edits. Now add the Toolbar for the frequent actions: new task, indent and outdent, delete, reorder. It comes from the same package and binds to the same API:

<script setup lang="ts">
import { ref, onMounted } from "vue";
import type { IApi } from "@svar-ui/vue-gantt";
import { Gantt, Willow, Editor, Toolbar } from "@svar-ui/vue-gantt";
import { RestDataProvider } from "@svar-ui/gantt-data-provider";
import "@svar-ui/vue-gantt/all.css";
// ...
</script>

<template>
<Willow>
<Toolbar :api="api" />
<Gantt :tasks="tasks" :links="links" :scales="scales" :init="init" />
<Editor :api="api" />
</Willow>
</template>

Each button triggers a chart action, and every action runs through setNext. The Toolbar needs no extra code to persist its changes.

8. Handle errors

Two places can fail: the initial load and the writes.

Loading errors. Add .catch() to the getData() call:

server.getData()
.then((data) => {
tasks.value = data.tasks;
links.value = data.links;
})
.catch((error) => {
console.error("Failed to load data:", error);
});

Save errors. A failed save surfaces through the chart's event system. Two recommendations:

  • Treat the server as a safety net, not your primary validation. Catch a bad link or an impossible date on the client before it's ever sent.
  • If a server error reaches the UI, notify the user and reload fresh data from the database. This keeps the chart in sync without complex reconciliation.

The full component

The following snippet shows the complete GanttChart.vue with all pieces assembled:

<script setup lang="ts">
import { ref, onMounted } from "vue";
import type { IApi } from "@svar-ui/vue-gantt";
import { Gantt, Willow, Editor, Toolbar } from "@svar-ui/vue-gantt";
import { RestDataProvider } from "@svar-ui/gantt-data-provider";
import "@svar-ui/vue-gantt/all.css";

// highlight weekend cells in the day scale
function dayStyle(date: Date) {
const weekend = date.getDay() === 5 || date.getDay() === 6;
return weekend ? "sday" : "";
}

const scales = [
{ unit: "month", step: 1, format: "%F %Y" },
{ unit: "day", step: 1, format: "%j", css: dayStyle },
];

// point the provider at the Nuxt server API routes
const server = new RestDataProvider("/api");

const api = ref<IApi | null>(null);
const tasks = ref<any[]>([]);
const links = ref<any[]>([]);

onMounted(() => {
server.getData().then((data) => {
tasks.value = data.tasks;
links.value = data.links;
});
});

function init(ganttApi: IApi) {
api.value = ganttApi;
// route every Gantt action through the REST provider
ganttApi.setNext(server);
}
</script>

<template>
<Willow>
<Toolbar :api="api" />
<Gantt :tasks="tasks" :links="links" :scales="scales" :init="init" />
<Editor :api="api" />
</Willow>
</template>

We extended the client-only chart with:

  • Data loading via RestDataProvider.getData(): pulls tasks and links from the server and parses their dates.
  • Automatic saves through api.setNext(server): every chart action maps to a REST call.
  • Order that outlives a reload: moves rewrite orderId on the server through the operation: "move" PUT.
  • A toolbar whose buttons work through the same pipeline.

The data provider handles the transport layer. Implement standard REST endpoints for your database, and the Gantt connects automatically.