DEV Community

Marco Iamonte
Marco Iamonte

Posted on

Sample TodoApp in Fresh Deno framework

New day and, yet, another framework. Yes, that's exactly how it has been for a few years, so why not trying it out?

Recently, the fresh framework came out and I've seen a whole lot of articles, drama and excitement about it, which made me wondering "fine, what's new about this?". So, I've decided to head to the homepage and find out what the hype is for, what's the good and what is (of course) the "bad".

Heading to the website, you will immediately notice the awesome landing with the drop animation, which is a great start.
Then, reading the core points, you will guess that the framework seems to be trying to take place where other bigger frameworks might fall: simplicity, clever SSR, reduced tooling and less overhead.

Most of these points are something that usually triggers most of the frontend and full stack developers that needs to deliver performant, scalable, efficient and maintainable solutions: most of the times you spend most of the time dealing with node_modules, making someone else able to compile your project because for unknown reasons he is using an older version of node (or a newer one), end up having your local build running but failing on the CI server for unknown reasons, needing to be able to customise the head tags for the SEO and other subtle things that makes the whole experience a bit less appealing that it should be.

The goal of fresh, to me, seems to be reducing or removing the above steps, and it actually does it by:

1) Not using node (it uses Deno instead).
2) Gain appeal because it uses and supports preact out of box.
3) Remove the build step.

These three points are, in my opinion, the three reasons you might want to choose this framework over any other alternative (like nextjs, nuxtjs or whatever other one you like): it's simple, it handles basic features well, it's easy to use and reduces to the minimum the amount of javascript on the client side.

I've tried the fresh framework yesterday and actually tried to replicate a project that was previously made in Vue and, although I had some small difficulties, the final project came out pretty well and the performances are actually admirable, though I do have some regrets of choosing this over nextjs which was, in this case, probably more suitable.

Because every single time I see a tech news I usually see someone trying to run Doom on something new, I usually feel like for web devs the Doom equivalent is the Todo app. So, why not making one in Fresh and share the result? But hey, everyone would be able to do a Todo app, so why not adding complexity to it? Therefore, I came up with this idea: I want to create a fresh project that...:

  • Shows how island works, meaning that some part of the project must use islands.
  • Shows how things can be accomplished without Islands (and where).
  • Shows how even an API can be developed with fresh.
  • Deploy the application using Docker instead of the default way proposed by the fresh (using Deno deploy).

So, in the next chapters, we will create an application that aims to show the core features of Fresh, with an extra attention on the backend layer, trying to focus on the core layer of the application, the data layer of the application and, finally, the application layer, by still keeping the opinionated structure created by the fresh authors.

The next chapters assumes Deno is already installed in your environment.

The final result will look like this:

Image description

The entire code that is explained and provided in this post can be found here: https://github.com/briosheje/Fresh-Deno-Mongo-Docker-Todoapp


Chapter 1: scaffolding a project

To scaffold a project, simply run deno run -A -r https://fresh.deno.dev my-app.
In our case, since we already created a repository and already are in the target folder, we will rather run: deno run -A -r https://fresh.deno.dev .

. specifies to create the project in the current folder.

You might see this message: The target directory is not empty (files could get overwritten). Do you want to continue anyway? [y/N] Hit yes in case you have nothing you need in the folder, otherwise move what you need and run the command again: the folder where you create a new project must be empty.

For the UI layer, we will be using tailwind as it is shipped out with Fresh and as far as I could see, it's the only feasible solution right now, so when the prompt ask this:
Do you want to use 'twind' (https://twind.dev/) for styling? [y/N]

Answer y.

If you inspect the created folders and data (using, for example ls in MacOS) you will see that the core project has been created and that the core dependencies have been downloaded.
The project created will have the following folders:

  • routes: This is where routing happens and is handled. If you come from nextjs or similar frameworks, this will be extremely familiar: each file matches a route (or a pattern), each folder matches a sub route. Routes can be handler, allowing you to handle the verb and what the route actually do and might render something or return a simple response. More on this can be directly checked on the documentation, I won't focus much on this.
  • static: This is where static assets are placed. Static assets are resolved using / in html templates and css files and caching can be handled using asset (read more about caching here).
  • utils: This folder contains various stuff, right now out of box it comes with a tailwind wrapper for the final application.
  • islands: This is the most unique folder of the framework, since it contains preact components that will be hydrated. This folder cannot have subfolders and preact components cannot have complex props like Functions, Date instances and anything which isn't a primitive or something JSON-serializable.

To run the project and check everything is working, run deno task start, which will be running the start task in the deno.json file, which basically runs the dev.ts file and watches for changes on static and routes.

Also, side note but in my opinion relevant note, fresh seems to prefer the import_map approach with Deno, meaning that dependencies are mapped in a file and referred in code through dependency names rather than the URL to the dependency, so we will follow the preferred approach in the next chapters.

If everything is running properly, we can procede with the next chapter: preparing to work with Docker.

Chapter 2: MongoDB and Docker for the dev environment.

Because the fresh framework is agnostic about this, I will be doing this in my way, which is based on my experience: in our case, we will surely need a database. Whichever the database is (spoiler: it will be MongoDB), I usually like to virtualise everything where possible, but I also like to keep multiple ways of accomplishing the same task for my collaborators, so we will set up this project with:

  • A docker-compose for development where the entire folder is mounted and Deno self-refreshes upon changes.
  • A docker-compose for production use where the environment is used to pass secrets to the application.

For both environments, the application will need to know where the database is, so we are going to use environment variables to do so, therefore we will install a dependency called dotenv by adding it to the import_map.json file: in case you are using visual studio code to develop, remember to cache the dependency.
Since we will be using mongo, we will also install mongo.

Our import_map.json file will then look like this:

{
  "imports": {
    "mongo": "https://deno.land/x/mongo@v0.30.1/mod.ts",
    "dotenv": "https://deno.land/x/dotenv@v3.2.0/mod.ts",
    "$fresh/": "https://deno.land/x/fresh@1.0.0/",
    "preact": "https://esm.sh/preact@10.8.1",
    "preact/": "https://esm.sh/preact@10.8.1/",
    "preact-render-to-string": "https://esm.sh/preact-render-to-string@5.2.0?deps=preact@10.8.1",
    "@twind": "./utils/twind.ts",
    "twind": "https://esm.sh/twind@0.16.17",
    "twind/": "https://esm.sh/twind@0.16.17/"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now that the dependencies are setup, let's procede by creating an environment file, adding it to .gitignore and creating a .env.example file to guide future users through the creation of the initial .env file.

The .env file will be structured like this:

MONGODB_URI=
Enter fullscreen mode Exit fullscreen mode

Where the value of the environment variable will point to the desired mongodb instance.

Our .env file will also be ignored since it will only be used in development, so let's add this to the .gitignore (or create one if it does not exist yet):

.env
Enter fullscreen mode Exit fullscreen mode

Once this is done, we will create a new folder where the core of our backend lives in, and we will be naming the folder core. Inside the core folder, we will create a file called app-env.ts where we will handle the environment and export variables:

import { config as dotEnvConfig } from "dotenv";
dotEnvConfig({
  export: true,
});

const appEnv = {
  MONGODB_URI: Deno.env.get("MONGODB_URI"),
};
const everyEnvVariableFilled = Object.values(appEnv).every(
  (v) => v !== null && v !== undefined && v !== "" && !Number.isNaN(v)
);
if (!everyEnvVariableFilled) {
  console.error(
    `Not all env variables are correctly compiled, please check that each env variable has a value.`
  );
  Deno.exit(1);
}

export default appEnv;
Enter fullscreen mode Exit fullscreen mode

In this way, the process will gracefully exit with an error if an environment variable is missing and needed to start the application.

In the core folder, we will create a data folder where we will be exposing the mongodb client. In a better architecture the data layer would be separated from the application layer, but because we are just making an example app I'm not going to separate this layer now.

We will create the following structure:

  • /data/models will hold the mongo/application models (in this case, these will be shared).
  • /data/mongo-client.ts will export the client.
  • /data/collections.ts will be an enum with the supported collections (currently one)
  • /data/[entity] will hold utility functions for the specified database entity, like create, get, list and other actions.

mongo-client.ts:

import { MongoClient } from "mongo";
import appEnv from "../app-env.ts";

const { MONGODB_URI } = appEnv;

const client = new MongoClient();

if (!MONGODB_URI) {
  console.error(`MONGODB Uri missing. Exiting.`);
  Deno.exit(1);
}

await client.connect(MONGODB_URI);
const database = client.database("TODO_APP");

export default database;
Enter fullscreen mode Exit fullscreen mode

The above file will use the environment to know where to connect to.

collections.ts:

export enum Collections {
  TODOS = "todos",
}
Enter fullscreen mode Exit fullscreen mode

models/todo.ts:

import { ObjectId } from "mongo";

export interface Todo {
  _id: ObjectId;
  name: string;
  done: boolean;
}

export type NewTodo = Omit<Todo, "_id">;

export function isNewTodo(item: any): item is NewTodo {
  return Boolean(item?.name) && !item._id;
}
Enter fullscreen mode Exit fullscreen mode

Please note that we will also be providing utility methods like the type guard isNewTodo in the models, these will be needed later.

todo/create-todo.ts

import { Collections } from "../collections.ts";

import { Todo, NewTodo } from "../models/todo.ts";
import client from "../mongo-client.ts";

export default function createTodo(todo: NewTodo) {
  return client.collection<Todo>(Collections.TODOS).insertOne(todo);
}
Enter fullscreen mode Exit fullscreen mode

todo/get-todo.ts

import { Collections } from "../collections.ts";

import { ObjectId } from "mongo";
import { Todo } from "../models/todo.ts";
import client from "../mongo-client.ts";

export default function getTodo(id: string) {
  return client.collection<Todo>(Collections.TODOS).findOne({
    _id: new ObjectId(id),
  });
}
Enter fullscreen mode Exit fullscreen mode

todo/list-todo.ts

import { Collections } from "../collections.ts";

import { Todo } from "../models/todo.ts";
import client from "../mongo-client.ts";

export default function listAllTodos() {
  return client.collection<Todo>(Collections.TODOS).find().toArray();
}
Enter fullscreen mode Exit fullscreen mode

todo/toggle-todo-done.ts

import { ObjectId } from "mongo";

import { Collections } from "../collections.ts";
import { Todo } from "../models/todo.ts";
import client from "../mongo-client.ts";
import getTodo from "./get-todo.ts";

export default async function toggleTodoDone(todoId: string) {
  const collection = client.collection<Todo>(Collections.TODOS);
  const todoDoc = await getTodo(todoId);

  if (!todoDoc) {
    throw new Error(`Record with id ${todoId} was not found.`);
  }

  return collection.updateOne(
    {
      _id: new ObjectId(todoId),
    },
    {
      $set: {
        ...todoDoc,
        done: !todoDoc.done,
      },
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

Please note that all the above methods were wrote down just to test the framework, these should be written better in a production-ready environment.

This data layer will allow us to later retrieve and alter data from the database, but because we actually need a database, we will take advantage of docker and the docker environment plus Makefile commands to create routines that allow us to test everything locally.

Because we are planning to support at least two environments, we will be shipping two docker-compose files: although the production environment might not use docker-compose, we will still create a sample one for production.

In our dev docker-compose, we want to have a stack that includes mongo and a UI to manage the database. I wish I could also mount Deno and use it with Fresh, but it looks like it currently does not work, so our docker-compose-dev.yml will look like this:

version: "3.4"

services:
  todo-mongo-dev:
    image: mongo:latest
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    ports:
      - 27017:27017
    networks:
      - todo-app-dev-network

  mongo-express:
    image: mongo-express:latest
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
      ME_CONFIG_MONGODB_URL: mongodb://root:example@todo-mongo-dev:27017/
    networks:
      - todo-app-dev-network
networks:
  todo-app-dev-network:
Enter fullscreen mode Exit fullscreen mode

Because we are using this only in localhost, we don't care about any environment variable and any secure credential: this stack will be only used in development mode.

Next step is configuring our Deno application to know what is the url to connect for the mongo database, and this is where the .env file we have created before will be needed: in our .env file we will define the mongodb url so that our application can connect to the database, so our .env file will now become this:

MONGODB_URI=mongodb://root:example@127.0.0.1:27017/
Enter fullscreen mode Exit fullscreen mode

In this way, when the Deno application runs, dotenv will load the environment variables from the .env file directly.

To run the stack, we will create the following Makefile and run the run-dev command:

run-dev:
    docker-compose -f ./docker-compose-dev.yml down
    docker-compose -f ./docker-compose-dev.yml rm
    docker-compose -f ./docker-compose-dev.yml build
    docker-compose -f ./docker-compose-dev.yml up -d --remove-orphans
    # Sleep below is needed to wait for mongo to start, otherwise we would need 
    # to handle that in code.
    sleep 5
    deno task start

dev-down:
    docker-compose -f ./docker-compose-dev.yml down
    docker-compose -f ./docker-compose-dev.yml rm
Enter fullscreen mode Exit fullscreen mode

Running make run-dev should take up the stack (or down in case it is up and then up again), should wait 5 seconds (which should be enough for mongo to start listening) and then will run deno task start to run the stack that will load the .env file and connect to the database.

We now should be good to go with the next chapter: wiring the client and the server.

Chapter 3: wiring client and server

Now that we have a database inside our stack and we have our data layer done, we can focus on the frontend and the backend: because this is a demo, the core idea is to show the features of the framework, therefore we are going to implement the following:

1) Form testing: use a POST form submit to the same page to create a new Todo record.
2) API testing: make an api endpoint like /api/v1/todo/[id]/toggleState with PUT verb to toggle the state of a todo. In this case, a page refresh should not occur.

Before doing both, however, we will need to first display the current todos, so our "index" will first need to do so:

/routes/index.tsx

/** @jsx h */
import { h } from "preact";
import { tw } from "@twind";
import { Handlers, HandlerContext, PageProps } from "$fresh/server.ts";

import { Todo } from "../core/data/models/todo.ts";
import listAllTodos from "../core/data/todo/list-todo.ts";

type TodosProps = {
  allTodos: Todo[];
};

export const handler: Handlers<TodosProps> = {
  async GET(_req, ctx) {
    const allTodos = await listAllTodos();

    return ctx.render({
      allTodos: allTodos ?? [],
    });
  },
}

export default function Todos(props: PageProps<TodosProps>) {
  const { data } = props;
  const { allTodos } = data;

  return (
    <div
      className={tw`h-100 w-full flex flex-col items-center justify-center bg-teal-lightest font-sans`}
    >
      <div
        className={tw`bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg`}
      >
        <h2>Todo Items - Items uses the API endpoints to update data.</h2>
        <hr className={tw`mb-1`} />
        {allTodos.map((todo) => (
          <span>TODO todo island component</span>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The above code will handle the GET request, fetch all the current todos (beware: a distributed cache here would be awesome) and render the page by injecting in a react-way the current todos. Everything you see above happens at server side, no javascript will be injected to the client side and the template is compiled at server side.

Once the todos are ready, we then need to populate these, so we need to:

  • Create a form
  • Find a way to handle the form submit on the same page

To accomplish so, we will start with a simple form, which will be placed under our Todo list:

      <hr />
      <div
        className={tw`bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg`}
      >
        <h2>
          Create a new todo: this approach uses a SUBMIT action against the same
          page.
        </h2>
        <form method="POST" className={tw`flex flex-col font-sans`}>
          <input />
          <div className={tw`mb-4`}>
            <label
              className={tw`block text-gray-700 text-sm font-bold mb-2`}
              for="todo-name"
            >
              Name
            </label>
            <input
              className={tw`shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline`}
              id="todo-name"
              type="text"
              name="name"
            />
          </div>
          <button
            className={tw`bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline`}
            type="submit"
          >
            Add todo
          </button>
        </form>
      </div>
Enter fullscreen mode Exit fullscreen mode

This form will perform submit a POST request against our index, so we will handle that and make it re-render the page with the list of the current todos, so our handler will become:

export const handler: Handlers<TodosProps> = {
  async GET(_req, ctx) {
    const allTodos = await listAllTodos();

    return ctx.render({
      allTodos: allTodos ?? [],
    });
  },
  async POST(req, ctx) {
    const formData = await req.formData();

    const jsonData = Object.fromEntries(formData);
    if (isNewTodo(jsonData)) {
      await createTodo({
        ...jsonData,
        done: false,
      });
    }
    const allTodos = await listAllTodos();

    return ctx.render({
      allTodos: allTodos ?? [],
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Note that we will also need to import isNewTodo, and that in the req object we can actually fetch the form data from the client: through he form data, the record is created and all the records are returned.

Hence, upon a post request, the server will create the record in case it is a valid Todo item and will render the page with the updated list of Todo Items.

Next, we need to handle the second point: we should allow the final user to toggle the state of a todo, and for the sake of testing the framework we want to do that through an api endpoint (although it's of course unnecessary).

To accomplish so, we will create 4 nested folders inside the routes folder: api, v1, todo and [todoId], next we will create inside the [todoId] folder the toggleState.ts file: the final endpoint will be /api/v1/todo/[todoId]/toggleState, and toggleState.ts will hold this code:

import { Handlers } from "$fresh/server.ts";

import toggleTodoDone from "../../../../../core/data/todo/toggle-todo-done.ts";

export type ToggleStateResponse = {
  modifiedCount: number;
};

export const handler: Handlers<ToggleStateResponse> = {
  async PUT(_req, ctx) {
    const { modifiedCount } = await toggleTodoDone(ctx.params.todoId);

    return new Response(
      JSON.stringify({
        modifiedCount,
      }),
      {
        headers: {
          "content-type": "application/json",
        },
      }
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

As you can see, differently from the previous index file, we are only using the Handler to actually handle the PUT method against this route, to alter the data on the database and to return a simple response with content-type application/json.

Finally, we should now handle how the client will request the change to the backend and, because it is preact... I do that with an hook.

Because such hook might be used somewhere else for whatever reasons, I usually like to keep ui elements and utilities in a separate folder, so we are going to make a ui folder with an hooks folder inside it, so that we can later use it.

Since our endpoint returns an information that we don't need, we will just need to handle three scenarios:

  • The request is pending (loading).
  • The request raised an error.
  • The request ended with a positive status.

To handle all of these, our /ui/hooks/useToggleTodoState.tsx hook will look like this:

/** @jsx h */
import { h } from "preact";
import { useCallback, useState } from "preact/hooks";

export default function useToggleTodoState(onSuccess: () => void) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error>();

  const toggleTodoState = useCallback(
    (todoId: string) => {
      setLoading(true);
      setError(void 0);

      return fetch(`/api/v1/todo/${todoId}/toggleState`, {
        method: "PUT",
      })
        .then((data) => {
          setLoading(false);
          if ([200, 204].includes(data.status)) {
            onSuccess();
          }
        })
        .catch((error) => setError(error))
        .finally(() => setLoading(false));
    },
    [setLoading, setError, onSuccess]
  );

  return {
    loading,
    error,
    toggleTodoState,
  };
}
Enter fullscreen mode Exit fullscreen mode

Please note the onSuccess callback that is meant to be invoked upon a successful response: this hook will allow us to invoke the API endpoint and update the status of a todo, now we just need to wire things up with a component that displays the todo and allows us to update its status.

Because the component is meant to add interaction to the index page, it will be located in the islands folder and will be hydrated after the page is rendered: therefore, since our component will render a TodoItem, we will call it TodoItem.tsx and it will be like this:

/** @jsx h */
import { h } from "preact";
import { useCallback, useState } from "preact/hooks";
import { tw } from "@twind";

import useToggleTodoState from "../ui/hooks/useToggleTodoState.tsx";
import { Todo } from "../core/data/models/todo.ts";

export type TodoItemProps = {
  value: Todo;
};

export default function TodoItem(props: TodoItemProps) {
  const { value } = props;

  const [done, setDone] = useState(value.done);

  const onStatusUpdated = useCallback(() => {
    setDone((prev) => !prev);
  }, [setDone]);

  const { loading, error, toggleTodoState } =
    useToggleTodoState(onStatusUpdated);

  const toggleState = useCallback(() => {
    toggleTodoState(value._id.toString());
  }, [value]);

  return (
    <div className={tw`flex mb-4 items-center`}>
      <p className={tw`w-full text-grey-darkest`}>{value.name}</p>
      <button
        disabled={loading}
        onClick={toggleState}
        className={tw`flex-no-shrink p-2 ml-4 mr-2 border-2 rounded text-green border-green hover:bg-green`}
      >
        {done ? "Mark as not done" : "Mark as done"}
      </button>
      {error}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

As you can see it just looks like a regular react component, although you might notice that it is using models taken from the core folder but because in the models file we are using nothing incompatible it won't affect the rendering of the page.

Now, we just need to use the component in our homepage, so we will replace the list placeholder with this item and our homepage list section will be this:

   <div
        className={tw`bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg`}
      >
        <h2>Todo Items - Items uses the API endpoints to update data.</h2>
        <hr className={tw`mb-1`} />
        {allTodos.map((todo) => (
          <TodoItem value={todo} />
        ))}
      </div>
Enter fullscreen mode Exit fullscreen mode

Now that everything is wired up, we will also configure Docker to run in a production-like environment.

Chapter 4: Dockerfile and configuring for production

In a production environment, two steps should be taken:

1) Build the docker image and tag it.
2) Use it in some way.

In our case, we will be assuming for testing purposes that for scenario 2 we will be using docker-compose, but this solution actually works with anything that can actually run an image.

First of all, we will need to write our Dockerfile:

FROM denoland/deno:alpine-v1.23.0

EXPOSE 8000
WORKDIR /app

COPY . .

# Cache dependencies
RUN deno cache main.ts --import-map=import_map.json

# No need to check about --allow-all because it's already sandboxed
CMD ["run", "--allow-all", "main.ts"]
Enter fullscreen mode Exit fullscreen mode

Briefly explained: we are using the 1.23.0 alpine Deno image, exposing port 8000 (used by fresh by default), caching the dependencies and running main.ts. This will listen on port 8000 and won't watch any file.

Now, we need to make a docker-compose that uses the image an we need to tag it. For this example, we will assume our image will be named fresh-todo-app-test.

Our docker-compose will look like this:

version: "3.4"

services:
  todo-app-fresh:
    image: fresh-todo-app-test:latest
    container_name: todo-app-fresh
    restart: always
    ports:
      - ${PUBLIC_PORT:-8000}:8000
    environment:
      MONGODB_URI: mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@todo-mongo:27017/
    networks:
      - todo-app-network

  todo-mongo:
    image: mongo:latest
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD}
    networks:
      - todo-app-network
    volumes:
      - mongo-data:/data/db

  mongo-express:
    image: mongo-express:latest
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGODB_USERNAME}
      ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGODB_PASSWORD}
      ME_CONFIG_MONGODB_URL: mongodb://${MONGODB_USERNAME}:${MONGODB_PASSWORD}@todo-mongo:27017/
    networks:
      - todo-app-network

networks:
  todo-app-network:

volumes:
  mongo-data:
Enter fullscreen mode Exit fullscreen mode

Please not that the environment in docker-compose is set up assuming that the mongodb username and password are in the .env file, so we will also need to update our .env file to have these:

.env:

MONGODB_URI=mongodb://root:example@127.0.0.1:27017/
MONGODB_USERNAME=root
MONGODB_PASSWORD=test
Enter fullscreen mode Exit fullscreen mode

the first variable will be used locally, while the second and third variables will be used in docker-compose.

Now that everything is set up, we will update our Makefile with the commands needed to build and deploy the stack:
Makefile

build-and-tag-image:
    docker build -t fresh-todo-app-test .

deploy-stack:
    docker-compose -f ./docker-compose.yml --env-file .env down
    docker-compose -f ./docker-compose.yml --env-file .env rm
    docker-compose -f ./docker-compose.yml --env-file .env build
    docker-compose -f ./docker-compose.yml --env-file .env up -d --remove-orphans

build-and-deploy-stack: build-and-tag-image deploy-stack

stack-down:
    docker-compose -f ./docker-compose.yml --env-file .env down
    docker-compose -f ./docker-compose.yml --env-file .env rm
Enter fullscreen mode Exit fullscreen mode

Now, running make build-and-deploy-stack you should see the stack coming up and it should be available in seconds, you might notice some restarts in case the Deno app starts but the mongodb database is not yet ready.

Stack

Finally, the application should be ready and we did a todo app with fresh!

Chapter 5: conclusions

Fresh seems to be a very interesting framework with, in my opinion, a very specific target: monolithic applications (or at least something with frontend and backend together) with SSR on steroids and an opinionated structure with a particular attention on keeping things simple but at the same time incredibly elastic.

It's a pleasure, in my opinion, to see something built on Deno that is currently at least trying to shade a bit the current huge javascript frameworks, although I'm feeling that many targets are still way more suitable for next.js and similar frameworks.

However, since we talked about great things only, I feel we should also mention that there actually are scenarios where the framework does not yet shine that much, which is everything where styling is involved.

Currently, the framework supports tailwind out of box, but in case you want to use a simple custom CSS you will need to wrap your head around assets and manually do assets optimisation, meaning that there is literally no tooling around this issue. To give a comparison, next.js compiles .scss files and whatever kind of style you want through react and will ship the bundled styles by tree shaking and, thus, optimising the final bundle.

That said, it looks like this framework can have a bright future, I hope you've found the post useful!

Discussion (0)