DEV Community

Yoni Rapoport
Yoni Rapoport

Posted on • Edited on

Build a Full-stack CRUD App using Deno's Fresh and Postgres

Fresh is a full-stack framework for Deno that makes it easy to develop apps with TypeScript. Let's use it to build a simple Todo app with full CRUD capabilities.

The resulting code of this (very) short tutorial can be found in this GitHub repository.

To get started, make sure you have the Deno CLI version 1.23.0 or higher installed.

Step 1 - Create a new Fresh Project

deno run -A -r https://fresh.deno.dev todos
cd todos
deno task start
Enter fullscreen mode Exit fullscreen mode

You can now open http://localhost:8000 in your browser to view the page.

Step 2 - Speed things up with a Full-stack CRUD framework

To build the CRUD operations fast and efficiently, I'll use Remult, a CRUD framework that was built for Node.js but also works great with Deno.

(Note: I'm one of the creators of Remult.)

Let's add Remult to the project's import_map.json:

{
  "imports": {
    // ...
    "remult": "https://cdn.skypack.dev/remult?dts",
    "remult/remult-fresh": "https://cdn.skypack.dev/remult/remult-fresh?dts",
    "https://deno.land/std/node/fs.ts": "https://deno.land/std@0.177.0/node/fs.ts"
  }
}
Enter fullscreen mode Exit fullscreen mode

Remult is bootstrapped as a Fresh middleware, let's create a _middleware.ts file under routes/ with the following code:

// routes/_middleware.ts

import { remultFresh } from "remult/remult-fresh";

export const remultServer = remultFresh({ }, Response);

export const handler = remultServer.handle;
Enter fullscreen mode Exit fullscreen mode

Step 3 - Create the Task model

Create a model folder under root, and create a task.ts file with the following code:

// model/task.ts

import { Entity, Fields } from "remult";

@Entity("tasks", {
    allowApiCrud: true
})
export class Task {
    @Fields.uuid()
    id!: string;

    @Fields.string()
    title = '';

    @Fields.boolean()
    completed = false;
}
Enter fullscreen mode Exit fullscreen mode

For remult to provide a CRUD API for the Task model, we need to register it in the entities array of the remult server:

// routes/_middleware.ts

import { remultFresh } from "remult/remult-fresh";
import { Task } from "../model/task.ts";

export const remultServer = remultFresh({
  entities: [Task]
}, Response);

//...
Enter fullscreen mode Exit fullscreen mode

πŸš€ At this point the CRUD API is ready. (Test it at http://localhost:8000/api/tasks.)

Step 4 - Create an Island for Displaying and Editing Todos

Since the todo list is going to be pretty interactive, I'll use a Fresh island to display and edit the list. However, I'll use Fresh's Server-Side Rendering to fetch the initial list of tasks when the page loads. πŸ’ͺ

Create a islands/todos.tsx file with the following code:

// islands/todos.tsx

/** @jsx h */
import { h } from "preact";
import { Remult } from "remult";
import { useState } from "preact/hooks";
import { Task } from "../model/task.ts";

const remult = new Remult();
const taskRepo = remult.repo(Task);

export default function Todos({ data }: { data: Task[] }) {
  const [tasks, setTasks] = useState<Task[]>(data);

  const addTask = () => {
    setTasks([...tasks, new Task()]);
  };

  return (
    <div>
      {tasks.map((task) => {
        const handleChange = (values: Partial<Task>) => {
          setTasks(tasks.map((t) => t === task ? { ...task, ...values } : t));
        };

        const saveTask = async () => {
          const savedTask = await taskRepo.save(task);
          setTasks(tasks.map((t) => t === task ? savedTask : t));
        };

        const deleteTask = async () => {
          await taskRepo.delete(task);
          setTasks(tasks.filter((t) => t !== task));
        };

        return (
          <div key={task.id}>
            <input
              type="checkbox"
              checked={task.completed}
              onClick={(e) => handleChange({ completed: !task.completed })}
            />
            <input
              value={task.title}
              onInput={(e) => handleChange({ title: e.currentTarget.value })}
            />
            <button onClick={saveTask}>Save</button>
            <button onClick={deleteTask}>Delete</button>
          </div>
        );
      })}
      <button onClick={addTask}>Add Task</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Modify the default routes/index.ts file to query the database for the list of tasks in the SSR handler, and pass it to the Todos island when rendering the page:

// routes/index.ts

/** @jsx h */
import { h } from "preact";
import { Handlers, PageProps } from "$fresh/server.ts";
import Todos from "../islands/todos.tsx";
import { Task } from "../model/task.ts";
import { remultServer } from "./_middleware.ts";

export const handler: Handlers<Task[]> = {
  async GET(req, ctx) {
    const remult = await remultServer.getRemult(req);
    return ctx.render(await remult.repo(Task).find());
  },
};

export default function Home({ data }: PageProps<Task[]>) {
  return (
    <div>
      <Todos data={data} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Hard part is over 😌

At this point, we have a todo list and we can create, update, and delete tasks.

Go ahead and create a few tasks, when you save them you'll notice a db folder with a tasks.json file has been created in the project folder. This JSON database is remult's default database which is nice for quick prototyping.

Step 5 - Connect to Postgres and Deploy

  1. We'll need a new Postgres instance to connect to. I used these instructions from the Deno Deploy docs to set up an instance on Supabase.

  2. There are several ways to deploy apps to Deno Deploy. I chose to push my code to GitHub and connect a Deno Deploy project to the GitHub repository using the Deno Deploy dashboard.

    Don't forget to add a DATABASE_URL environment variable in the Deno Deploy project settings, with the value of the connection string provided by Supabase.

  3. Now let's tell remult to use the DATABASE_URL environment variable to connect to Postgres.

// routes/_middleware.ts

import { remultFresh } from "remult/remult-fresh";
import { Task } from "../model/task.ts";
import { createPostgresConnection } from "https://deno.land/x/remult/postgres.ts";

export const remultServer = remultFresh({
  entities: [Task],
  dataProvider: async () => {
    const dbUrl = Deno.env.get("DATABASE_URL");
    if (dbUrl) {
      return createPostgresConnection({ connectionString: dbUrl });
    }
    return await undefined;
  },
}, Response);

export const handler = remultServer.handle;
Enter fullscreen mode Exit fullscreen mode

You can test the Postgres connection locally by creating a .env file in the project's root folder with the content: DATABASE_URL='<YOUR_CONNECTION_STRING>'.

Once you commit and push this last change to your GitHub repository, your todo app will be deployed and connected to your Postgres database. ✨

The End

I've tried to make this post as short and easy to follow as possible. If I've missed something important please leave a comment so I can fix it.

Top comments (3)

Collapse
 
awalias profile image
awalias

Loved this post! Just want to mention something around GET: we don't allow GET requests that display html documents on our Edge functions (Deno).

This post is only using Supabase for database, but I wanted to clarify in case someone assumes they could deploy the fresh framework on our Edge functions too.

Collapse
 
varshneytech profile image
\/// πŸ†ƒπŸ…΄πŸ…²πŸ…·

Step 5.1
In Supabase, after setting up project, on left side column,
it's:
Bottom Left (Gear Icon) Settings β†’ Database β†’ Connection Pooling

not Database β†’ Connection Pooling

took me a few minutes to figure this out!
Image description

Collapse
 
yonirapoport profile image
Yoni Rapoport

Thanks!

Fixed the link to a more up-to-date version of Deno Deploy docs about setting up Postgres.