DEV Community

Rishabh Shukla
Rishabh Shukla

Posted on

Supabase + Express + Drizzle ORM

Hey folks how's it going? Hope you're well and taking care of your loved ones.

I recently explored how to set up a Node Express backend with Supabase and Drizzle ORM using JavaScript. If you're just starting or prefer JavaScript at the backendโ€”this one's for you ๐Ÿ˜Š๐Ÿ˜Š.

Note: This guide assumes you're familiar with backend and frontend development and some level of full stack work.


๐Ÿ› ๏ธ Project Setup

0. Supabase Setup

  1. Create a Supabase account and confirm via email.
  2. Create an organization (default name comes from your email).
  3. Choose Personal type and Free plan.
  4. Name your project (e.g., Todo) and set a strong password (youโ€™ll need it).
  5. Choose your region and hit Create Project.

1. Backend Setup ๐ŸŒ

  • Create folders: Todo/backend and Todo/frontend.
  • In terminal:
  cd backend
  npm init
Enter fullscreen mode Exit fullscreen mode

Fill details or use:

package name: (backend) todo-backend
description: "A simple backend for Supabase"
entry point: (index.js)
keywords: backend, supabase
author: Rishabh Shukla
license: (ISC)
type: (commonjs) module
Enter fullscreen mode Exit fullscreen mode

Install dependencies:

npm install express drizzle-orm cors dotenv postgres uuid @supabase/supabase-js nodemon
Enter fullscreen mode Exit fullscreen mode

Add dev script in package.json:

"scripts": {
  "dev": "nodemon src/index.js"
}
Enter fullscreen mode Exit fullscreen mode

Setup .env with DATABASE_URL

From Supabase Dashboard โ†’ Connect โ†’ ORMs โ†’ Drizzle โ†’ copy DATABASE_URL. Add to .env:

DATABASE_URL=your_connection_string
CORS_ORIGIN=http://localhost:5173
Enter fullscreen mode Exit fullscreen mode

drizzle.config.js

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/models',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL,
  },
});
Enter fullscreen mode Exit fullscreen mode

Folder structure now:

|- backend
   |- node_modules
   |- .env
   |- drizzle.config.js
   |- package.json
   |- src/
|- frontend
Enter fullscreen mode Exit fullscreen mode

Frontend Setup ๐Ÿ“ฑ

cd frontend
npm create vite@latest .
# Choose React + JavaScript
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

๐Ÿง  Coding Part

Backend Code

src/index.js

import express from 'express';
import cors from 'cors';
import 'dotenv/config';
import { db } from './db/index.js';
import { todos } from "./models/todo.model.js";
import { v4 as uuidv4 } from 'uuid';
import { eq } from "drizzle-orm";

const app = express();
const port = 3000;

app.use(cors({ origin: process.env.CORS_ORIGIN || "*", credentials: true }));
app.use(express.json());

app.get('/', (req, res) => res.send('Hello World!'));

app.post("/todo", async (req, res) => {
  const { todo } = req.body;
  if (!todo) return res.status(400).send("Todo is required");
  try {
    const task = await db.insert(todos).values({
      id: uuidv4(),
      todo,
      created_at: new Date(),
      updated_at: new Date(),
    }).returning();
    res.status(201).json(task);
  } catch (err) {
    console.log(err);
  }
});

app.get("/get/todos", async(req, res) => {
  try {
    const allTodos = await db.select().from(todos);
    res.status(200).json(allTodos);
  } catch (err) {
    console.log(err);
  }
});

app.patch("/todo/:id", async (req, res) => {
  const { id } = req.params;
  const { todo } = req.body;
  try {
    await db.update(todos).set({ todo, updated_at: new Date() }).where(eq(todos.id, id));
    res.status(200).json("Todo updated successfully!");
  } catch (err) {
    console.log(err);
  }
});

app.delete('/todo/:id', async (req, res) => {
  const { id } = req.params;
  try {
    await db.delete(todos).where(eq(todos.id, id));
    res.status(200).json("Todo deleted successfully!");
  } catch (err) {
    console.log(err);
  }
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

src/db/index.js

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import "dotenv/config";

const connectionString = process.env.DATABASE_URL;
const client = postgres(connectionString, { prepare: false });
export const db = drizzle(client);
Enter fullscreen mode Exit fullscreen mode

src/models/todo.model.js

import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";

export const todos = pgTable("todos", {
  id: uuid("id").primaryKey().defaultRandom(),
  todo: text("title").notNull().unique(),
  created_at: timestamp("created_at").notNull().defaultNow(),
  updated_at: timestamp("updated_at").notNull().defaultNow().$onUpdateFn(),
});
Enter fullscreen mode Exit fullscreen mode

Generate SQL and apply in Supabase SQL Editor:

npx drizzle-kit generate
Enter fullscreen mode Exit fullscreen mode

Copy generated SQL โ†’ paste in Supabase SQL editor โ†’ click Run.


Frontend Code

App.jsx

import { useState } from 'react';
import Form from './Form';
import List from './List';

function App() {
  const [todo, setTodo] = useState("");
  const [todoList, setTodoList] = useState([]);
  const [isReadyForUpdate, setIsReadyForUpdate] = useState(false);
  const [updatedTodo, setUpdatedTodo] = useState("");

  const handleUpdate = (todo) => {
    setIsReadyForUpdate(true);
    setUpdatedTodo(todo.id);
    setTodo(todo.todo);
  };

  return (
    <>
      <Form
        todo={todo}
        setTodo={setTodo}
        isReadyForUpdate={isReadyForUpdate}
        updatedTodo={updatedTodo}
      />
      <hr/>
      <List todoList={todoList} setTodoList={setTodoList} handleUpdate={handleUpdate} />
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Form.jsx

import axios from 'axios';

const Form = ({ todo, setTodo, isReadyForUpdate, updatedTodo }) => {
  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = isReadyForUpdate
        ? await axios.patch(`http://localhost:3000/todo/${updatedTodo}`, { todo })
        : await axios.post("http://localhost:3000/todo", { todo });
      console.log(response);
      setTodo("");
      window.location.reload();
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <form className='form' onSubmit={handleSubmit}>
      <label htmlFor='Todo'>Todo</label>
      <input
        type="text"
        placeholder="Todo"
        name='Todo'
        className='input'
        onChange={(e) => setTodo(e.target.value)}
        value={todo}
      />
      <input type="submit" value={isReadyForUpdate ? "Update" : "Add"} className='add-button' />
    </form>
  );
};

export default Form;
Enter fullscreen mode Exit fullscreen mode

List.jsx

import axios from "axios";
import { useEffect } from "react";

const List = ({ todoList, setTodoList, handleUpdate }) => {
  useEffect(() => {
    const getTodos = async () => {
      const response = await axios.get("http://localhost:3000/get/todos");
      setTodoList(response.data);
    };
    getTodos();
  }, [setTodoList]);

  const handleDelete = async (id) => {
    await axios.delete(`http://localhost:3000/todo/${id}`);
    window.location.reload();
  };

  return (
    <div>
      {todoList.map((todo) => (
        <div key={todo.id} className="todo-list-container">
          <p>{todo.todo}</p>
          <div className="button-container">
            <button onClick={() => handleUpdate(todo)}>Update</button>
            <button onClick={() => handleDelete(todo.id)}>Delete</button>
          </div>
        </div>
      ))}
    </div>
  );
};

export default List;
Enter fullscreen mode Exit fullscreen mode

โœ… Conclusion

You now have a working full stack Todo App using Supabase, Drizzle ORM, and Express.js, all written in JavaScript!

๐Ÿ”— GitHub Repo

๐Ÿ“ท Instagram
๐ŸŽฏ LinkedIn

If you have any problem consider asking in comments. Like, share and follow

Image description

Top comments (0)