Build a Full-stack CRUD App using Deno's Fresh and Postgres
Tags: deno, showdev, typescript, tutorial
Published: 2022-07-28 17:46:00

[Fresh](https://fresh.deno.dev/) 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](https://github.com/yoni-rapoport/fresh-remult-todo).

To get started, make sure you have the [Deno CLI](https://deno.land/) 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
```

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](https://github.com/remult/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`:

```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"
  }
}
```

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

```ts
// routes/_middleware.ts

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

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

export const handler = remultServer.handle;
```

## Step 3 - Create the Task model

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

```ts
// 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;
}
```

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

```ts
// routes/_middleware.ts

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

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

//...
```

:rocket: 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. :muscle:

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

```tsx
// 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>
  );
}
```

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:

```tsx
// 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>
  );
}
```

### Hard part is over :relieved:

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](https://deno.com/deploy/docs/postgres#setup-postgres) from the Deno Deploy docs to set up an instance on [Supabase](https://supabase.com/).

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](https://dash.deno.com/projects).
> 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.

```ts
// 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;
```

> 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**. :sparkles:

## 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.

* To learn more about Deno's Fresh framework go to https://fresh.deno.dev.
* To learn more about Remult go to https://remult.dev.