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
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"
}
}
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;
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;
}
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);
//...
π 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>
);
}
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>
);
}
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 atasks.json
file has been created in the project folder. This JSON database isremult
's default database which is nice for quick prototyping.
Step 5 - Connect to Postgres and Deploy
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.
-
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. Now let's tell
remult
to use theDATABASE_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;
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.
- To learn more about Deno's Fresh framework go to https://fresh.deno.dev.
- To learn more about Remult go to https://remult.dev.
Top comments (3)
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.
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!
Thanks!
Fixed the link to a more up-to-date version of Deno Deploy docs about setting up Postgres.