DEV Community

Cover image for Building a full stack app with Deno Fresh and Fauna
Shadid Haque
Shadid Haque

Posted on

Building a full stack app with Deno Fresh and Fauna

Building a full stack app with Deno Fresh and Fauna

The best way to learn about new technology is by building something practical. In this tutorial, you will build a simple blogging platform with the Fresh framework and explore various features of Fresh.

Fresh is a next-generation JavaScript framework built on top of Deno. Fresh requires no configuration and comes with cool features such as just-in-time rendering, no build step, out-of-box Typescript support, and island architecture (also known as partial hydration).

You can play around with the live demo app here. You can also find the complete code for this tutorial in this Github repository.

Pre-requisites

  • Deno 1.x
  • Familiarity with React
  • Some Familiarity with TypeScript

Creating a Fresh App

Run the following commands to scaffold a new Fresh application.

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

Visit localhost:8080 and you are presented with a new Fresh app.

Fresh

Open the project in your favourite code editor. You will observe a folder structure similar to the following one.

VS Code

If you are familiar with React you will feel right at home with Fresh. Fresh uses Preact, a minimal version of React.

Fresh uses server side rendering. It compiles javascript in the server side and ships pure HTML to the client. Parts of a server-rendered page can then be independently re-hydrated with interactive widgets (islands). This architecture pattern is called island architecture. You can learn more about Fresh architecture here.

Creating a Navbar and application layout

Create a new file components/NavComponent.tsx and add the following code.

// components/Navbar.tsx

const logoStyle = `inline-flex items-center border-b-2 border-purple-500 px-1 pt-1 text-sm font-medium text-gray-900`;

export default function Navbar() {
  return (
    <nav class="bg-white shadow">
      <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <div class="flex h-16 justify-between">
          <div class="flex">
            <div class="ml-6 flex space-x-8">
              <a 
                href="/"
                class={logoStyle}
              >
                Fresh Blogs 🌿
              </a>
            </div>
          </div>
        </div>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Next, create a new file, routes/_app.tsx and add the following code.

// routes/_app.tsx

import { AppProps } from "$fresh/server.ts";
import NavComponent from "../components/Navbar.tsx";

export default function App({ Component }: AppProps) {
  return (
    <>
      <NavComponent />
      <Component />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

You define your application layout in this file. Notice that you are importing the NavComponent in the _app.tsx file. This makes the NavComponent visible in every pages of your application.

Next, go ahead and update the index.tsx file with following code.

// routes/index.tsx

export default function Home() {
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="my-6 text-3xl">
        Welcome to `🥤 Fresh Blogs 🍋`!
      </h1>
      <p class="my-6">Fresh ideas everyday</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Welcome to Fresh blogs

Now you have an application layout. Next, let’s go ahead and create some page routes.

Creating new routes (New Post page)

Create a new file routes/posts/new.tsx and add the following code.

// routes/posts/new.tsx 

import { PageProps } from "$fresh/server.ts";
import { Fragment } from "preact";

export default function NewPostPage(props: PageProps) {
  return (
    <Fragment>
      <div class="p-4 mx-auto max-w-screen-md">
        New Post Page
      </div>
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

In the code above you are exporting a new react component.

Fresh automatically generates a new route for your application when you create a new react component inside the routes directory. Observe the fresh.gen.ts file and you will notice that your newly created routes are added to this file automatically.

Visit the /posts/new route and notice that the component above gets rendered.

You can also create a handler function in your routes file. The handler function will be executed on the server side whenever someone visits the route. Here's an example.

// routes/posts/new.tsx 
import { PageProps } from "$fresh/server.ts";
import { Fragment } from "preact";
import { HandlerContext, Handlers } from "$fresh/server.ts";

export const handler: Handlers = {
  GET(req: Request, ctx: HandlerContext) {
    console.log('Inside GET handler');
    return ctx.render({
      message: "Hello New Post 📝",
    });
  },
};

export default function NewPostPage(props: PageProps) {
  return (
    <Fragment>
      <div class="p-4 mx-auto max-w-screen-md">
        {props.data.message}
      </div>
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

You can also pass data from your handler function to your component using the context render function as shown in the previous code block.

Next, let’s create a form to create new posts. A form has client side interactivity. In fresh you use islands to handle client side interactivity.

Islands are isolated Preact components that are rendered on the client. Islands are defined by creating a file in the islands/ folder in a Fresh project. Islands are re-hydrated as needed which gives Fresh the extra performance boost unlike other SSR solutions for React.

Create a new file islands/PostForm.tsx and add the following code.

// islands/PostForm.tsx

import { useState } from "preact/hooks";
export const inputStyle = `p-4 border-2 border-purple-400 radius rounded-md flex w-9/12`;

export default function PostForm() {

  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const createPost = (e: Event) => {
    e.preventDefault();
  }

  return (
    <div class="p-5 m-5">
      <h1 class="text-xl">Post Form</h1>
      <div class="mt-1 flex">
        <input 
          class={`${inputStyle}`} 
          placeholder="Post Title"
          onChange={(e: any) => setTitle(e.target.value)}
          value={title}
        />
      </div>
      <div class="mt-1 flex">
        <textarea 
          class={`${inputStyle}`} 
          placeholder="Write something..."
          onChange={(e: any) => setContent(e.target.value)}
          value={content}
        />
      </div>
      <button 
            onClick={createPost}
            class="rounded-md mt-3 border-transparent bg-purple-200 px-4 py-2"
          >
            Submit
          </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Import the island component in the routes/posts/new.tsx file.

// ...routes/posts/new.tsx

import PostForm from "../../islands/PostForm.tsx";

//...

export default function NewPostPage(props: PageProps) {
  return (
    <Fragment>
      <div class="p-4 mx-auto max-w-screen-md">
        <PostForm />
      </div>
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

Dynamic routes (View Post page and Edit Post page)

Fresh supports dynamic routes similar to Next.js. Folder structure determines the routing mechanism. Create a new file routes/posts/[id]/index.tsx and add the following code.

// routes/posts/[id]/index.tsx
import { useState } from "preact/hooks";
import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
  GET(_req, ctx) {
    const { id } = ctx.params;
    return ctx.render({
      content: `Hello Post ${id} 📝`,
    });
  }
};

export default function PostPage(props: PageProps) {
  const [post, setPost] = useState<any>();
  setPost(props.data);

  if(!post) { 
    return <div>Loading...</div>
  }
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1>{post.content}</h1>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Notice that the directory has a folder with [<name>] syntax. Based on the folder structure the application sets up a /posts/:id route. Visit http://localhost:8000/posts/1 to make sure the age is loading as expected.

Hello Worlds Program

Let’s create an edit post page next. Create a new file routes/posts/[id]/edit.tsx and add the following code.

// routes/posts/[id]/edit.tsx

import { Handlers, PageProps } from "$fresh/server.ts";
import PostForm from "../../../islands/PostForm.tsx";
import { Fragment } from "preact";

export const handler: Handlers = {
  GET(_req, ctx) {
    const { id } = ctx.params;
    return ctx.render({
      data: { id },
    });
  },
};

export default function EditPostPage(props: PageProps) {
  return (
    <Fragment>
      <div class="p-4 mx-auto max-w-screen-md">
        <PostForm />
      </div>
    </Fragment>
  )
}
Enter fullscreen mode Exit fullscreen mode

You now have all the pages ready. Next, let’s go ahead and add a database to the project so you can save and retrieve posts.

Adding Fauna database to the project

Head over to fauna.com and create a new account if you haven’t done already. Next create a new database.

Navigate to Security > Keys in your fauna dashboard. Select New Key to generate a new key.

Generate Security Key

Select Admin as role. Give your key a name and select Save.

Generating keys

You will recieve a key 🔑 like following. Do not share this key.

fnAEx.....
Enter fullscreen mode Exit fullscreen mode

Configuring the Database

For this demo application you need 3 Collections in the database. User, Post and Comment. Additionally you need to define relationship between these collections. There is a has_many relationship between User to Post and Post to Comment. Fauna also provides out-of-box user registration and secure authentication.

You can create all these collections from Fauna dashboard in the UI or you can run this setup script to generate all the resources for you. Copy the script into utils/fauna-setup.ts file.

To run the script first export your fauna admin secret and domain in the shell.

$ export FAUNA_ADMIN_SECRET=<Your-admin-secret> \
  export FAUNA_DOMAIN=<Your-db-domain>
Enter fullscreen mode Exit fullscreen mode

Next run the script.

$ deno run ./utils/fauna-setup.ts
Enter fullscreen mode Exit fullscreen mode

What does the script generates?

When the script execution is complete all the collections and indexes will be created in your Fauna database. The script sets up several Fauna resources. It sets up three collections User Post and Comment. It also creates an index to search user by email.

The script will also create a server secret for your application. Your application will use this secret to communicate with the database. Export the server secret by running following command in your terminal.

export FAUNA_SECRET=fnAExE....
Enter fullscreen mode Exit fullscreen mode

You can also create a .env file to store these environment variables variable. Following is an example of a .env file. Always make sure not to commit this file with your source control.

FAUNA_DOMAIN=db.us.fauna.com
FAUNA_SECRET=fnAEuxxxx.....
FAUNA_ADMIN_SECRET=fnADxxxxx..... 
Enter fullscreen mode Exit fullscreen mode

Finally, you can visit the Fauna dashboard and explore the generated resources in the UI.

Collection

Indexes

Create a new file utils/db.ts and add the following code. Here you are instantiating a new database client. You use these helper functions to query the database.

// utils/db.ts
import * as faunadb from "https://deno.land/x/fauna@5.0.0-deno-alpha9/mod.js";

export const q = faunadb.query as any;

export const faunaClient = new faunadb.Client({ 
  domain: Deno.env.get("FAUNA_DOMAIN"),
  secret: Deno.env.get("FAUNA_SECRET"),
});

export const getFaunaClient = (secret: string) => new faunadb.Client({
  domain: Deno.env.get("FAUNA_DOMAIN"),
  secret,
});
Enter fullscreen mode Exit fullscreen mode

Setting up user registration

Now that you have the database configured let's go ahead and implement user registration and user login functionality.
First, create a new route for user login. Create a new file, routes/signup.tsx and add the following code.

// routes/signup.tsx
import SignupForm from "../islands/SignupForm.tsx";

export default function UserSignup() {
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="my-6 text-3xl">
        🚀 Let's get you signed up!🚀 
      </h1>
      <SignupForm />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notice you have a SignupForm island imported in this page. Go ahead and create a new island islands/SignupForm.tsx and add the following code.

// islands/SignupForm.tsx

import { useState } from "preact/hooks";
import { inputStyle } from "./PostForm.tsx";

export default function SignupForm() { 
  const [state, setState] = useState({});

  const handleChange = (e: any) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  }

  const register = async () => { 
  }

  return (
    <div>
      <div class="pl-4 pt-4 mt-4">
        <input 
          onChange={handleChange} 
          type="text" 
          class={`${inputStyle}`} 
          placeholder="Name" 
          name="username"
        />
      </div>
      <div class="pl-4 pt-4 mt-1">
        <input 
          onChange={handleChange} 
          type="email" 
          class={`${inputStyle}`} 
          placeholder="Email" 
          name="email"
        />
      </div>
      <div class="pl-4 pt-2 mt-1">
        <input 
          onChange={handleChange} 
          type="password" 
          class={`${inputStyle}`} 
          placeholder="Password" 
          name="password"
        />
      </div>
      <div class="pl-4 pt-2 mt-1">
        <button 
          onClick={register} 
          class="rounded-md mt-3 border-transparent bg-purple-200 px-4 py-2"
        >
          Register
        </button>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

On the Register button click, you want to make a call to Fauna to register your users. You can make a direct HTTP call from your SignupForm island component. However, you will expose the Fauna's secret key by doing this. It is a good practice to make database interactions and third-party API calls from the server side. This way, your API secrets are never exposed on the client side.

You can create a new API route in Fresh, a server-side REST endpoint, then have your island component make a call to this API endpoint and handle all database interactions in the endpoint.

Diagram

Fresh API routes are similar to Next.js API routes.

Create a new file routes/api/register.ts and add the following code.

import { Handlers } from "$fresh/server.ts";
import { faunaClient, q } from "../../utils/db.ts";

export const handler: Handlers = { 
  async POST(req: Request) {
    try {
      const body = await req.json();

      const newUser = await faunaClient.query(
        q.Create(
          q.Collection("User"),
          {
            credentials: { password: body.password },
            data: {
              email: body.email,
              username: body.username,
            },
          }
        )
      );

      return Response.json({
        data: {
          ...newUser.data,
        }
      });
    } catch (error) {
      return Response.json({
        error: error.message,
      });
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Here you are creating a new User record in Fauna. You can learn more about Fauna CRUD operations in the official docs here.

Next, plug this api endpoint to your SignupForm register function as shown in the following code snippet.


// islands/SignupForm.tsx
export default function SignupForm() { 
  // ...
  const register = async () => { 
    try {
      const response = await fetch("/api/register", {
        method: "POST",
        body: JSON.stringify({
          ...state,
        }),
      });
      const data = await response.json();
      console.log(data);
      alert("Successfully registered!, try to log 🪵 in now");

    } catch (error) {
      alert("Something went wrong!");
    }
  }

  return (
    <div>
      ...
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:8000/signup and try to register a new user. Once a new user is registered it should appear under User collection in Fauna.

Lets get signed up image

Collection Populated

Notice that your password is not saved as a string. This is because you defined password field as credentials type. This is a part of Fauna’s secure built in authentication feature.

User Login

You can follow the same pattern to setup user login functionality. Create a new file routes/login.tsx and add the following code.

// routes/login.tsx

import LoginForm from "../islands/LoginForm.tsx";
export default function UserLogin() {
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="my-6 text-3xl">
        Login to your account and start blogging! 📝
      </h1>
      <LoginForm />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Create a LoginForm island component to handle the form.

Next, create a new api routes to handle Login.

// routes/api/login.ts

import { Handlers } from "$fresh/server.ts";
import { faunaClient, q } from "../../utils/db.ts";

export const handler: Handlers = { 
  async POST(req: Request) {
    try {
      const body = await req.json();

      const authUser: any = await faunaClient.query(
        q.Login(
          q.Match(q.FaunaIndex("users_by_email"), body.email),
          { password: body.password  },
        )
      );

      return Response.json({
        data: {
          token: authUser.secret,
        }
      });
    } catch (error) {
      console.log('==>>>', error);
      return Response.json({
        error: error.message,
      });
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

And finally create a LoginForm island to handle interactivity.

// islands/LoginForm.tsx

import { useState } from "preact/hooks";
import { inputStyle } from "./PostForm.tsx";

export default function LoginForm() { 
  const [state, setState] = useState({});

  const handleChange = (e: any) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  }

  const doLogin = async () => {
    try {
      const response = await fetch("/api/login", {
        method: "POST",
        body: JSON.stringify({...state}),
      })
      const data = await response.json();
      if(data.error) {
        alert(`${data.error} : Make sure you have a correct email and password`);
      } else {
        localStorage.setItem("token", data.data.token);
        alert("Successfully logged in!");
      }
    } catch (error) { 
      console.log(error);
      alert("Something went wrong!");
    }
  }

  return (
    <div>
      <div class="pl-4 pt-4 mt-4">
        <input 
          onChange={handleChange} 
          type="email" 
          class={`${inputStyle}`} 
          placeholder="Email" 
          name="email"
        />
      </div>
      <div class="pl-4 pt-2 mt-1">
        <input 
          onChange={handleChange} 
          type="password" 
          class={`${inputStyle}`} 
          placeholder="Password" 
          name="password"
        />
      </div>
      <div class="pl-4 pt-2 mt-1">
        <button 
          onClick={doLogin} 
          class="rounded-md mt-3 border-transparent bg-purple-200 px-4 py-2"
        >
          Signin
        </button>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

You use the Login function from Fauna to generate a temporary user secret (it is invalidated after some time). You save this secret in your local storage just like a jwt token. You need to pass this secret with your API call to Fauna when making protected requests (i.e. create or delete posts).

Head over to http://localhost:8000/login and try logging in with a registered user.

Notice on successful login the Fauna secret is saved in the local storage.

Notice Token

ℹ️ You can learn more about Fauna’s identity based authentication here.

Adding CRUD for Posts

Following a similar pattern, you can add CRUD operations for posts. Create a new API route to handle post CRUD operations. Create a new file routes/api/post.ts and add the following code.

// routes/api/post.ts

import { Handlers } from "$fresh/server.ts";
import { getFaunaClient, q } from "../../utils/db.ts";

export const handler: Handlers = {
  /**
   * Create a new post
   */
  async POST(req: Request) {
    try {
      const body = await req.json();
      const faunaClientWithAuth = getFaunaClient(req.headers.get("Authorization")!);
      const newpost = await faunaClientWithAuth.query(
        q.Create(
          q.Collection('Post'),
          { 
            data: { 
              ...body, 
              owner: q.CurrentIdentity(), 
              author: q.Select(["data", "username"], q.Get(q.CurrentIdentity()))
            } 
          },
        )
      );

      return Response.json({
        data: {
          _id: newpost.ref.id,
          ...newpost.data,
        }
      });
    } catch (error) {
      return Response.json({
        error: error.message,
      });
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

In the previous code block, you use the getFaunaClient function and pass in the Authorization header. The Authorization header is a Fauna secret that you save in your local storage on successful login.

On PostForm island component plug in the api call when submit button is clicked.

// islands/PostForm.tsx
...
export default function PostForm() {

  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");

  const createPost = async (e: Event) => {
    e.preventDefault();
    const response = await fetch("/api/post", {
      headers: {
        "Authorization": localStorage.getItem("token") || "",
      },
      method: "POST",
      body: JSON.stringify({ title, content, author: localStorage.getItem("username") }),
    });
    const data = await response.json();
    console.log(data);
    if(data.error) {
      alert('⛔ Something went wrong! Are you logged in?');
      return;
    }
    alert('Post created!');
    setTitle("");
    setContent("");
  }

  return (
    <div class="p-5 m-5">
      ...
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:8000/posts/new and try creating a new post. Make sure you are already logged in.

Showing a list of posts

In the home page a user should be able to view all the posts. In your routes/api/post.ts file add a new GET function to retrieve all the posts.

import { Handlers } from "$fresh/server.ts";
import { getFaunaClient, q, faunaClient } from "../../utils/db.ts";

export const handler: Handlers = {
  /**
   * Create a new post
   */
  async POST(req: Request) {
    //...
  },

   /**
   * Get all posts
   */
  async GET() {
    try {
      const posts: any[] = [];
      const { data } = await faunaClient.query(
        q.Map(
          q.Paginate(q.Documents(q.Collection('Post'))),
          q.Lambda("post", q.Get(q.Var("post")))
        )
      );
      data.forEach((post: any) => {
        posts.push({
          _id: post.ref.id,
          ...post.data,
        });
      })
      return Response.json({
        data: posts,
      });
    } catch (error) {
      return Response.json({
        error: error.message,
      });
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Create a new island component, islands/PostList.tsx to display all the posts.

// islands/PostList.tsx
import { useState, useEffect } from "preact/hooks";

export default function PostList() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch("/api/post");
        const remotePosts = await response.json();
        setPosts(remotePosts.data);
      } catch (error) {
        console.error(error);
        alert("Something went wrong!");
      }
    }
    fetchPosts();
  },[]);

  console.log(posts);

  return (
    <div class="p-5">
      <h1 class="text-xl">Post List</h1>
      {
        posts.map((post: any) => (
          <PostItem post={post} id={post._id} />
        ))  
      }
    </div>
  )
}

function PostItem({ post, id } : { post: any, id: string }) {
  return (
    <div class="p-5">
      <h4 class="text-md mb-3">{post.title}</h4>
      <a class="border mt-1 p-2 cursor:pointer" href={`/posts/${id}`}>View</a>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Add this island component into your index.tsx page.

// routes/index.tsx
import PostList from "../islands/PostList.tsx";

export default function Home() {
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="my-6 text-3xl">
        Welcome to `🥤 Fresh Blogs 🍋`!
      </h1>
      <p class="my-6">Fresh ideas everyday</p>
      <PostList />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

All the posts will now appear in your homepage.

Welcome to fresh blog

Viewing a single post

Notice that when you select the View button for any post from post list it takes you to the /post/:id route. In this page you can retrieve the post information and display it.

In this case, you can use the handler function from fresh to retrieve the data from the database. Since the handler function runs on the server side, you are not exposing any secure information. Make the following changes to your routes/posts/[id]/index.tsx file.

import { useState } from "preact/hooks";
import { Handlers, PageProps } from "$fresh/server.ts";
import { faunaClient, q } from "../../../utils/db.ts";

export const handler: Handlers = {
  async GET(_req, ctx) {
    const { id } = ctx.params;
    try {
      const post = await faunaClient.query(
        q.Get(q.Ref(q.Collection('Post'), id))
      );
      return ctx.render({
        _id: post.ref.id,
        ...post.data,
      });
    } catch (error) {
      return Response.json({
        error: error.message,
      });
    }
  },
};

export default function PostPage(props: PageProps) {
  const [post, setPost] = useState<any>();
  setPost(props.data);

  if(!post) { 
    return <div>Loading...</div>
  }
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="text-xl pl-4">{post.title}</h1>
      <div class="text-sm font-bold pl-4">By {post.author}</div>
      <p class="pl-4 text-left">{post.content}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Deleting a post

Add a DELETE function to your routes/api/post.ts api route to handle post delete.

// routes/api/post.ts
import { Handlers } from "$fresh/server.ts";
import { getFaunaClient, q, faunaClient } from "../../utils/db.ts";

export const handler: Handlers = {
  /**
   * Create a new post
   */
  async POST(req: Request) {
    ...
  },

   /**
   * Get all posts
   */
  async GET() {
    ...
  },
  /**
   * Delete a post
   */
  async DELETE(req: Request) {
    try {
      const body = await req.json();
      const faunaClientWithAuth = getFaunaClient(req.headers.get("Authorization")!);
      const post = await faunaClientWithAuth.query(
        q.Delete(q.Ref(q.Collection('Post'), body._id))
      );
      return Response.json({
        data: post.data,
      });

    } catch (error) {
      return Response.json({
        error: error.message,
      });
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

Next, make the Delete call from your page component to delete the post.

// routes/posts/[id]/index.tsx
// ...
export default function PostPage(props: PageProps) {
  const [post, setPost] = useState<any>();
  setPost(props.data);

  const deletePost = async () => {
    const response = await fetch(`/api/post/`, {
      headers: {
        "Authorization": `${localStorage.getItem("token")}`,
      },
      method: "DELETE",
      body: JSON.stringify({
        _id: post._id
      })
    });
    const data = await response.json();
    if(data.error) {
      alert(data.error);
    } else {
      alert("Post deleted!");
      window.location.href = "/";
    }
  }

  if(!post) { 
    return <div>Loading...</div>
  }
  return (
    <div class="p-4 mx-auto max-w-screen-md">
      <h1 class="text-xl pl-4">{post.title}</h1>
      <div class="text-sm font-bold pl-4">By {post.author}</div>
      <p class="pl-4 text-left">{post.content}</p>
      <button class="border mt-1 p-2 cursor:pointer" onClick={deletePost}>Delete</button>
    </div>
  )
} 
Enter fullscreen mode Exit fullscreen mode

You can also apply the same pattern and have CRUD for comments. You can follow the completed code for this project here to implement comments CRUD functionality.

Final thoughts

Fresh really takes a fresh new approach to web dev. It is still quite new but the ecosystem is rapidly growing. Up until now the Deno ecosystem was a missing a full stack framework and Fresh seems to fill that void quite effectively. You can create scalable, performant applications with Fresh and Fauna quite easily. On top of that you can make fully serverless full stack applications on the edge when you deploy your Fresh app to Deno deploy, denoflare.dev or Netlify.

Top comments (3)

Collapse
 
guigui64 profile image
Guillaume Comte

Great article thanks. I wonder where you found out about the special routes/_app.tsx file. I did not find it mentioned in the docs.

Collapse
 
shadid12 profile image
Shadid Haque

Think I saw it in one of the sample apps

Collapse
 
artydev profile image
artydev

Greatissime, thank you very much :-)