This project is a simple todo application built with SvelteKit and Convex, a type-safe backend-as-a-service (BaaS). It demonstrates how to implement CRUD operations (Create, Read, Update, Delete) in a modular way using Convex.
For more information, refer to the official Convex documentation.
Run the following command to create a new SvelteKit app:
npx sv create
Follow these steps in the setup prompts:
my-app
) or hit Enter for the current directory.SvelteKit minimal
.Yes, using TypeScript syntax
.npm
.Navigate to your project directory and install the required Convex packages:
cd my-app && npm install convex convex-svelte
Create a convex.json
file to set the Convex functions directory within src/
:
{
"functions": "src/convex/"
}
Run the following command:
npx convex dev
This will prompt you to log in with GitHub, create a project, and save your production and deployment URLs. A convex/
folder will be created for your backend API functions. The dev
command syncs your functions with your cloud dev deployment.
Create a sampleData.jsonl
file with the following content:
{"text": "Buy groceries", "isCompleted": true}
{"text": "Go for a swim", "isCompleted": true}
{"text": "Integrate Convex", "isCompleted": false}
Import the data into the database:
npx convex import --table tasks sampleData.jsonl
clientStore
Set up a store to manage the Convex client:
// src/lib/stores/clientStore.svelte.ts
import { ConvexClient } from "convex/browser";
interface ClientStore {
value: ConvexClient | null;
}
export let clientStore = $state<ClientStore>({ value: null });
Add Convex setup to your layout:
<script lang="ts">
// src/routes/+layout.svelte
import { PUBLIC_CONVEX_URL } from "$env/static/public";
import { clientStore } from "$lib/stores/clientStore.svelte";
import { setupConvex, useConvexClient } from "convex-svelte";
const { children } = $props();
setupConvex(PUBLIC_CONVEX_URL);
clientStore.value = useConvexClient();
</script>
<main>
{@render children()}
</main>
<style>
main {
min-height: 100dvh;
display: flex;
justify-content: center;
align-items: center;
}
</style>
Create a schema for the tasks
table:
// src/convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tasks: defineTable({
text: v.string(),
isCompleted: v.boolean(),
}),
});
Write backend functions for managing tasks:
// src/convex/tasks.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const getTasks = query({
handler: async (ctx) => ctx.db.query("tasks").collect(),
});
export const addTask = mutation({
args: { text: v.string(), isCompleted: v.boolean() },
handler: async (ctx, { text, isCompleted }) => {
await ctx.db.insert("tasks", { text, isCompleted });
},
});
export const editTask = mutation({
args: { id: v.id("tasks") },
handler: async (ctx, { id }) => {
const task = await ctx.db.get(id);
await ctx.db.patch(id, { isCompleted: !task?.isCompleted });
},
});
export const deleteTask = mutation({
args: { id: v.id("tasks") },
handler: async (ctx, { id }) => {
await ctx.db.delete(id);
},
});
Create utility functions for frontend task operations:
// src/lib/db/index.ts
import { api } from "$convex/_generated/api.js";
import { clientStore } from "$lib/stores/clientStore.svelte.js";
export function addTask(text: string) {
clientStore.value?.mutation(api.tasks.addTask, { text, isCompleted: false });
}
export function editTask(id) {
clientStore.value?.mutation(api.tasks.editTask, { id });
}
export function deleteTask(id) {
clientStore.value?.mutation(api.tasks.deleteTask, { id });
}
Create a form for adding tasks:
<script lang="ts">
// src/lib/components/CreateNewTask.svelte
import { addTask } from "$lib/db/index.js";
let newTask = $state("");
function handleSubmit(event: Event) {
event.preventDefault();
addTask(newTask);
newTask = "";
}
</script>
<form onsubmit={handleSubmit}>
<input type="text" bind:value={newTask} placeholder="New Task" />
<button type="submit">Add</button>
</form>
Render tasks in the UI:
<script lang="ts">
// src/routes/+page.svelte
import { useQuery } from "convex-svelte";
import { api } from "$convex/_generated/api.js";
import { editTask, deleteTask } from "$lib/db/index.js";
import CreateNewTask from "$lib/components/CreateNewTask.svelte";
const getTasksQuery = useQuery(api.tasks.getTasks, {});
</script>
{#if getTasksQuery.isLoading}
<p>Loading...</p>
{:else if getTasksQuery.error}
<p>Error: {getTasksQuery.error.message}</p>
{:else}
<CreateNewTask />
<ul>
{#each getTasksQuery.data as task (task._id)}
<li>
<input
type="checkbox"
checked={task.isCompleted}
onchange={() => editTask(task._id)}
/>
{task.text}
<button onclick={() => deleteTask(task._id)}>Delete</button>
</li>
{/each}
</ul>
{/if}
Run the following command:
npm run dev
Start the app, open http://localhost:5173 in a browser, and see the list of tasks.