Fresh is a web framework based on Deno and Web standards built to run on the edge
Fresh: a new full stack web framework for Deno
From:

Node Congress 2023
Transcription
Hi everyone, today we're going to talk about FRESH, a full stack web framework for deno. Before we get started, a little bit about me, as mentioned I work at Netlify, I'm from Montreal Quebec Canada. If you're looking for me online, I'm at NickyT online pretty much everywhere. If you want to know a little more about me, you can visit my website at imdeveloper.com. I also stream on Twitch, so if that's something you're interested in, you can check out imdeveloper.live and I also have a YouTube channel that you can check at youtube.imdeveloper.com. I am also not a big fan of spiders. Alright so what are we going to cover today? We're going to go over what FRESH is, we're going to discuss standards/talks">web standards, and then we'll dig into the features of FRESH. After that there will be a short demo and then we can move on to questions and comments. Alright let's get to it. So what is FRESH? Well hold on, first we need to talk about deno. So what is deno? deno is a runtime for javascript, typescript, and webassembly or WASM that uses V8. For the web, it runs on the edge. You can also use deno to create command line interfaces, i.e. CLIs. It's got a built-in linter, there's a built-in code formatter, a built-in test runner, there's node.js interoperability via Node specifiers, and there's also npm interoperability via npm specifiers and CDNs. Alright let's talk standards/talks">web standards. So deno uses standards/talks">web standards, for example, import maps, fetch, request, and response. Like the little drawing over there says, just look it up on MDN. Great docs, but also that's pretty much what you'll need to reference most of the time if you're working with FRESH. As part of standards/talks">web standards, deno is a part of the WinterCG, the Web Interoperable Runtimes community Group. It's a space to collaborate on api interoperability for javascript runtimes. Feel free to read more about the WinterCG at wintercg.org. Alright so where were we? Assuming that you have deno installed, getting FRESH installed is pretty quick. You can just run the command that you see on the slide deck here. So that's deno run-A-R and then https://fresh.deno.dev and the name of your project. That was a lot to say. Alright the installation is pretty quick and you have a couple of options. You can choose Tailwind for styles, go with VS Code integration via the deno VS Code extension, and that's pretty much it. We'll go more into the styling story a little later in the talk. To start FRESH, we go into the root folder of the project in a shell and run deno task start. We won't get into it in this talk, but deno has a built-in task runner that you can configure via a deno.json file. Alright so let's dig into what FRESH is. So what is FRESH? It's a full-stack web framework that runs on deno. It's server-side rendered framework. It's got just-in-time rendering on the edge. It provides typescript support out of the box and there's nothing to configure to get up and running. There's no build step. There's no javascript delivered by default. It uses islands architecture for client interactivity. It uses Preact on the server and client side. And there's JSX support thanks to Preact and typescript. If you're unfamiliar with the amazing Preact project, it's a fast and smaller alternative to react with the same modern api. It only gets a mention here for now, but we'll dig into what islands architecture is a little later in the talk. Alright let's go over some of the features of FRESH. We've got static files we'll talk about, routes and routing, data fetching, middleware, error pages, styling, and then we'll get to islands. So first up we have static files. All static assets can be found in the static folder. Static assets, aside from image and source tags, do not have cache headers set on them. You can set cache headers manually or you can use the built-in asset helper to automatically cache an asset for a year. Here's an example of the asset helper in action. So I have this on my style sheet. I'm using the asset helper and what happens is it generates a unique URL which is composed of the asset file path followed by a query string with one key, underscore FRSH underscore C with a value that is the build ID from the deployment. And you can see as I mentioned, there also gets this in the cache control headers set in the response. So we have it there for a year. Alright let's dig into routes and routing. There are three kinds of routes. There's the handler route, which is typically for APIs, the component route, which is for pages, and then the hybrid route. And for hybrid routes, it's for pages that require handler routes. For example, a login page or a search page. Like in other frameworks, routes can also be dynamic. You can see in the table here that I took from the FRSH documentation that there are many types of file based routing patterns that are supported. One thing to note though is FRSH only supports server side rendering, so there is no concept of something like get static paths in next.js or astro since pages are never statically generated. Alright let's look at the data fetching story. So for data fetching, we have handler routes which I just mentioned, and we can also use hybrid routes to handle data fetching. For a handler route, all that is required is exporting a function that takes two arguments, a request and a context, and it returns a response. So in this case here, we have a handler which is using the Jokes api that ships with the FRSH demo site, and we can see here it's just generating a list of random jokes from an array and then it's returning that as a response. For hybrid routes, we define functions for actions in an exported variable called handler. We name the async functions in the handler object after HTTP verbs, for example, get. Where as the handler route in the previous example returns a response, typically in a hybrid route you'll want to return the result of the context render method. Think of context render as passing in server side rendered props to the page being rendered. When I say props, I mean react props. Looking at the example on the slide, one thing to note about props in FRSH is that it's not props.movies, the props from context.render are always in the props.data property. So for example, the array of movie props that is actually props.data, it's not props.data.movies. That's a lot of dots in that talk. Alright we're going to move on to middleware. Often like other frameworks, FRSH supports middleware. Middleware files are named underscore middleware dot ts and need to reside in the routes folder. Multiple middlewares are supported. Although deno encourages you to use typescript, you could write the middleware in a javascript file. Let's look at an example. Say I navigate to slash movie slash Top Gun. Since we're hitting a dynamic movie route, that means we're using two middlewares. One in the root of the routes folder and one in the movie subfolder. In the case of multiple middlewares, the least specific one runs first. In our example, that means the root middleware runs first. It adds an x-conference header with the value node-congress-2023. Then the second middleware runs in the movie subfolder. It adds an x-movie-page header along with a cache header that caches the page for 60 seconds. If we navigate to slash movies, we can see the two middlewares giving a nice little handshake there. We can see the result in the response is that we get the cache control header and we get the two middlewares, the x-conference and the x-movie-page. Moving on, we're going to talk about error pages. Like other frameworks, you can define custom error pages. For example, the current slide shows a custom 404 page. These are component routes, but they have special props passed in. In the case of the 404 page, you have access to the unknown page props when the page is rendering. The unknown page props gives you access to things like the URL of the page that was not found. For a dynamic route, we call context.renderNotFound to render the 404 page if the page does not exist. Just to bring it back to data fetching, when we were discussing data fetching, I mentioned that we need to typically return the result of context.render for hybrid routes. That still holds, but as I mentioned, if you're on a page that is associated with a dynamic route and the page being loaded does not exist, we return context.renderNotFound instead. Context.renderNotFound will then pass the unknown page props that we were talking about a second ago to the 404 page. Let's move on to styling. When I first talked about Fresh, I mentioned that there was no build step. How does that affect styling our apps? Fresh gives you the option to enable TWIN, a server-side rendered implementation of Tailwind. This is great if you use Tailwind, but if the projects you're working on don't use Tailwind, what are your options? First, I would say modern css is pretty awesome these days. You could literally go with a good old style sheet. The fact that nested selectors are coming to browsers and we only have css variables and other goodies like has in css makes this a compelling choice. You could also add a build step that commits the build artifacts of your css tooling. You could accomplish this with a GitHub action or some other form of automation. Although it would work, it doesn't feel right committing build artifacts to the code base and it also goes against one of the promises of Fresh. No build step. I think what we're going to see here is innovation from userland where we may see more server-side rendered implementations of stuff like Sass, PostCSS, etc. much like the TWIN project. Another thing that I would love to see come to Fresh is Scope css. We have that in other frameworks like vue, svelte. I think that would be a great addition. All right, let's talk about islands. What is an island anyways? I mentioned it briefly when describing the architecture of Fresh, but we'll dig into island architecture a little more now. Long story short is it enables pockets of interactivity in your site or application. I'll leave this chunk of a blog post from Jason Miller up for a minute. But Jason Miller, one of the folks alongside Katie Seiler-Miller coined the term islands architecture. It's actually fitting that Jason is one of the folks that coined this term as he's also the creator of Preact, which Fresh uses. In islands architecture, the goal is to ship mainly static html and then mark certain regions of the document object model, i.e. the DOM, as available to hydrate or to use Fresh's terminology revive. When a page with islands loads, only the javascript for those islands is loaded. I mentioned that Fresh serves zero javascript to the client by default. So how do we enable the client-side interactivity that we're talking about in islands architecture? Fresh has two component folders. There's a components folder and an islands folder. Components in the components folder will always render server-side only, even if you add client-side interactivity to them. It just won't get sent down the javascript for the client-side interactivity. And components in the islands folder will render server-side as well as once the page loads and the client-side interactivity will be revived. So let's look at a classic interactive component, a counter. It has a couple of buttons. If you click plus one, it increments the counter. And if you click minus one, it decrements the counter. And in this case here, we have a couple. So how do we revive an island? So essentially what happens is the components that are islands get rendered server-side by Fresh. And if you were to do a view page source of the page that loaded, you will actually see the rendered markup for that particular island or islands. Along with the markup that's rendered for those islands, Fresh also adds a script of type application JSON with the ID underscore, underscore, FRSH, underscore, state as the ID. And in there, there is an array. In that array, the first element is another array, which is an array of all the props for every island that's in the page. The second element in the array is currently empty there, but that's for plugins. That's something we're not really going to touch on today. But if you're interested in the plugins, you can take a peek at the Fresh documentation. So as you can see here, I have JSX for my counter, and it starts off on the server-side where I say I'm passing in a prop with the value three for the start prop. And when it gets rendered on the page, we'll see that the initial props for that particular component get loaded in the array I mentioned. So how does it get revived? So we have that Fresh state that I just mentioned. And then one of the things that Fresh does is alongside the javascript related to that particular island, it has a revive function. And that revive function takes the list of the components. So for example, here, the counter, and it also passes in the first element of state, which is all the props for all the islands, as I mentioned. In the previous example, there was only one island. But if there was more than one island, how does Fresh map the initial state correctly? So if we have a couple here, we'll see that they just keep getting added to the array. But how does Fresh know that the counter with the start prop of three and the counter of start with prop five maps correctly to the array there? We talked about denoting interactive regions in the DOM when talking about islands architecture. Island components in Fresh get server-side rendered, as I mentioned, but they also get rendered with html comments surrounding them. The comment is prefixed with the FRSH dash followed by the name of the component, colon, and then the array index of where the island component state lives in the initial state for all islands that was initially loaded. It doesn't matter how many different island components there are or how many instances of each. The array index will be incremented in each html comment based on the order of the islands in the DOM. One thing to note is that this is just an implementation detail. You'll never need to manage this yourself. I just find it useful to understand the underlying technology. All right, we're going to move over to a short demo. So I'm just going to go ahead and move us over here. So what we have here is I just made a small demo site here. It's got a few pages, so the home here where we have some islands, so I have three counter components here. They're each managing their own state, and I also have a list of movies that goes to the movie route, and we can load up a movie, for example, like Top Gun. And if I open up the network panel here, let's refresh that again, and this has two middlewares running. And like I said, there was the first middleware in the routes root folder, and we can see that it has the x-conference header that that middleware provides. And then the subfolder middleware adds the x-movie-page header as well as the cache control of 60 seconds. All right, so we can close that. And here's an example of a hybrid page. So I'm just going to add a movie. So let's say Lord of the Rings, and I'll give it a rating of five, and I'm going to submit it. So we're on the same page, and the hybrid route has handlers in it in the handler object. And in this particular case, I'm passing a post, and that's what allows us to post back to the page and add the new movie here. For the purposes of this demo, I didn't use a database. It's just using a variable, so an in-memory database. But talking about data is totally out of scope of the talk, but just know that there's a lot of people working on the data problem on the edge. All right, so we're just going to kind of wrap up the demo here with... This is what the view page source looks like of that first page with the three different colored citrus fruits. I've taken out a lot of things just so that I can scroll through it. But the first thing is this is fresh just starting up here. That's what that script is. And you can see the folder here is based on the build ID. And then we can see that it only loads my counter javascript, which is for the island. And then we have my style, like I mentioned, and it's using the asset helper, and it's given it a unique ID, and it's got the... It's cached for a year, as I mentioned. I just wanted to show briefly here. So this is literally the code that got rendered minus I took out some SVGs for brevity. But we can see here, for example, that I have the first component here. It's denoted with the html comments, and then there's a second one and a third one. Then we have the fresh state here, and we can see there, the three there. And then we have that revive method down here, and we can see that it's running here. And we have our counter component, and then the state we're passing in, which is the props for all that. And that's pretty much the demo. All right, let's move back to the slide deck. And yeah, if you're interested in the demo, it's deployed at imdeveloper.com slash fresh-demo. You can also view the source code. It's at github.com slash nikityonline slash fresh-talk-demo. And we'll just finish off with just some resources that I think you'll all find useful. So there's a bunch of stuff about fresh, deno, Preact, some typescript, standards/talks">web standards, and also just some newer things with deno, like the node compatibility, and also a link to the winter CG. For folks interested, the slide deck is available at imdeveloper.com slash fresh. And that's pretty much it. We couldn't cover everything in about 20 minutes, but I hope this gave you all enough of a primer to get excited about fresh. Thanks, folks. ♪♪♪