Fresh 2: detailed navigation guide for developers?
Intro
I understand that Fresh 2 alpha version is not intended for unexperienced developers and might require reading the code sources. I am ready to read the code, however, I would still appreciate having some guidelines on where to look, and knowing what was changed in comparison to Fresh 1.x.
Can we collect here a list of resources and quick questions to navigate in Fresh 2?
What I am looking for currently:
1. An initial detailed description of the overall approach shift
- About the reinforced reliance on web standards API - where to learn about it more?
- What specific web standards are now more heavily used?
2. Are there any packages that could complement and benefit Fresh 2 development?
3. Quick API questions:
3.1. What is the recommended way to filter static resources vs routes in Fresh 2? I see that the Request has RequestDestination field; does it replace the DestinationKind?
3.2. I see that FreshContext misses route field now. Where is it? Should I parse URL manually?
These are simple questions just to start with. Would appreciate any pointers to documentation, source code files to look at, or examples.
If others have similar questions, please add them here so we can build a useful reference for Fresh 2 alpha developers.
UPDs:
- Realized that one of the most straightforward ways is to check JSR's auto-generated docs, e.g. for the current version: https://jsr.io/@fresh/[email protected]/doc (not much but symbols/methods with some amount of comments are there to explore).
The introductionary issue about Fresh 2 might be interesting and contains lots of background info https://github.com/denoland/fresh/issues/2363.
The only thing in the docs that has been updated for Fresh 2 so far is https://fresh.deno.dev/docs/canary/examples/migration-guide.
- An initial detailed description of the overall approach shift
Key improvement is that Fresh 2 uses an api similar to express and koa.
const app = new App()
.use(myMiddleware())
.use(someOtherMiddleware())
.get("/about", () => new Response("This is the about page"))
This is the way to add custom routes for apps and for plugins. Meaning there is no more special klunky plugin API. The plugin API is the same API you'd use outside of plugins and that Fresh itself uses. File system based routing is still supported via a plugin.
Testing also gets a lot easier. For the most part it will be just:
import { app } from "./main.ts";
const handler = app.handler();
Deno.test("Responds to /about", async () => {
const res = await handler(new Request("http://localhost/about"));
const text = await res.text();
expect(text).toContain("About Me")
})
There are some remaining details to figure out with islands, but this will be essentially it.
- Are there any packages that could complement and benefit Fresh 2 development?
Yes, that's basically the whole point of it. The plugin API in Fresh 1.x, while functional, sucks pretty much and is pretty unwieldy and limit in what you can do. This means it made it difficult to build an ecosystem around it and most changes would needed to be done right in Fresh itself. Fresh 2 solves that. You get all the info you need and the outer API is similar to existing frameworks. We use this here at Deno already much more extensively for custom addons for our internal purposes.
3.1. What is the recommended way to filter static resources vs routes in Fresh 2? I see that the Request has RequestDestination field; does it replace the DestinationKind?
The RequestDestination field is not needed anymore because you can intercept requests to static file by putting your middleware before the staticFiles() middleware and everything after it will never see those.
export const app = new App({ root: import.meta.url })
// This middleware handles and responds to static assets,
// anything after it won't see the request
.use(staticFiles())
// Consequently, this will never see requests for static assets
app.get("/my-route", ctx => {
return new Response("I'll never see static asset requests");
});
3.2. I see that FreshContext misses route field now. Where is it? Should I parse URL manually?
Good catch, we need to add this still. It's one of the reasons it's still marked as alpha because it's not done.
Again, the introduction issue contains a few background infos although some parts have changed the heart of it remains https://github.com/denoland/fresh/issues/2363
Thanks for your explanations, @marvinhagemeister!
I was exploring Koa API, and found this resource of middlewares: https://github.com/koajs/koa/wiki#middleware. Wanted to share. As I can see they are like a collection of middleware utilities for different cases like authentication, data parsing, security headers, logging, etc.
Looking deeper into this, I noticed that both Express and Koa have built ecosystems over the years. For example:
Express has middleware for almost everything:
- Security:
helmetfor security headers,cors,express-rate-limit - Auth:
passportwith 500+ login strategies, JWT middleware - Data:
body-parser,multerfor file uploads, validation - Logging:
morganfor requests,winstonfor application logs - And hundreds more...
Koa has similar but smaller ecosystem:
koa-helmet,@koa/cors,koa-router,koa-bodyparser, etc.
These aren't just "nice to have" - they solve real problems that every web app faces. Things like parsing request bodies, handling file uploads, validating data, securing with proper headers, logging requests, managing sessions, etc.
My question/assumption: Since Fresh 2 is adopting Express/Koa-like middleware patterns, will it also build (or directly adopt) a similar ecosystem of practical utilities?
Right now if I want to:
- Add security headers → Custom middleware
- Handle authentication flows → Custom implementation
- Log HTTP requests → Custom solution
- Handle file uploads → Figure it out from scratch
- Validate request data → Build from ground up
In Express/Koa, these are solved problems with battle-tested middleware.
Maybe I'm missing something, but it seems like Fresh 2 would benefit from either:
- Making it easy to adapt existing patterns from Express/Koa community;
- Building its own ecosystem of common middleware (like
@fresh/security,@fresh/logging, etc.); - Clear guidance on how to handle these common cases and how to adopt existing utilities.
What's the vision here? Will Fresh 2 eventually have this kind of "state of the art" middleware ecosystem, or is the expectation that developers build everything custom?
I think this could be valuable for the community to understand the roadmap - especially for those coming from Express/Koa backgrounds who are used to having these tools readily available.
Maybe I'm missing something, but it seems like Fresh 2 would benefit from either:
- Making it easy to adapt existing patterns from Express/Koa community;
- Building its own ecosystem of common middleware (like @fresh/security, @fresh/logging, etc.);
- Clear guidance on how to handle these common cases and how to adopt existing utilities.
What's the vision here? Will Fresh 2 eventually have this kind of "state of the art" middleware ecosystem, or is the expectation that developers build everything custom?
The reason we built Fresh 2, is to pave way for exactly this. There is now a possibility of building an ecosystem around it. This wasn't possible with Fresh 1. Whether there will be community provided middlewares or "official" ones remains to be seen. With the new API in Fresh it might even be possible to re-use many middlewares from the hono ecosystem.
Plugin Architecture Clarity Needed: Multiple APIs and "Plugin" Equivocation
While moving forward with my project migration, I'm experiencing confusion about Fresh 2 plugin system, and I think this might be a good clarification point for the community.
The Equivocation Problem
The word "plugin" refers to different concepts with different API signatures in Fresh 2:
- Runtime plugins (middleware):
function(app: App) { app.use(...) } - Build-time plugins:
function(builder, app, options) { ... }
Example of the inconsistency:
// Build-time plugin (Fresh 2's built-in) is called "plugin"
import { tailwind } from "@fresh/plugin-tailwind";
tailwind(builder, app, {}); // Takes builder, app, options
// Runtime plugin (from announcement) is called "plugin"
function disallowFoo(app: App) { // Takes only app
app.use((ctx) => { /* middleware */ });
}
And the old style for 3rd party "plugins":
export default defineConfig({
plugins: [
calculatorPlugin(),
],
});
Why do we have two different plugin signatures instead of a unified API like:
app.use(authMiddleware)app.with(tailwindPlugin, options)- Or some other better consistent pattern?
My Specific Case (demonstrating where the confusion comes from)
I'm using Fresh 2's built-in tailwind build-time plugin:
// dev.ts - build-time plugin signature
import { tailwind } from "@fresh/plugin-tailwind";
tailwind(builder, app, {});
But I need Tailwind CSS v4 @theme directive support:
/* This doesn't work with Fresh 2 built-in plugin */
@theme {
--color-brand: #123456;
}
Should I use @pakornv/fresh-plugin-tailwindcss instead? And if so, what signature does it expect? Should I opt for the old fashion fresh.config.ts for now then?
Summary
- Why to call different concepts "plugins" when they have different purposes and APIs? Can we clarify this somehow in docs and API as well?
- What's the intended API for community plugin developers - is it
function(builder, app, options)andfunction(app)depending on a plugin's purpose? - Will these plugin APIs be unified before stable release, or are multiple patterns here to stay?
- Is the old defineConfig({ plugins: [...] }) pattern still valid in Fresh 2 alpha?
- Do I miss something or does Fresh 2 built-in Tailwind plugin support Tailwind CSS v4 features like
@themedirective?
Why do we have two different plugin signatures instead of a unified API?
Fresh 1 had a unified plugin architecture. This lead to lots of projects pulling in developemnt only code into their production builds like esbuild, tailwind etc. None of that is needed during production and unnecessarily slow down deployments, cold starts etc.
For this very reason Fresh 2 has a strict split between stuff only needed during development (dev.ts and the Builder class) and stuff needed during production at runtime (main.ts, routes etc).
There might be plugins which need either one or both. Maybe we should introduce a nomenclature to differentiate the two. Haven't come up with something yet.
Should I use
@pakornv/fresh-plugin-tailwindcssinstead?
This plugin is written for Fresh 1.x . If you're using Fresh 1 you can use it, if you're using Fresh 2 you can't.
But I need Tailwind CSS v4 @theme directive support:
Tailwind v4 is not supported yet, as the plugin hasn't been updated yet. The plugin is currently build around Tailwind v3. See https://github.com/denoland/fresh/issues/2819
Should I opt for the old fashion fresh.config.ts for now then?
The fresh.config.ts is gone with Fresh 2.
I guess overall you're running into the issue that Fresh 2 is in alpha state so not every part has been addressed yet. Documentation isn't udpated, plugins aren't either, so there are some rough edges. Nonetheless, I'm excited for you giving this a go and sharing feedback 👍
Thanks for clarification! Maybe choosing clear nomenclature/terminology for the docs will be indeed enough. Just I suggest to make it very clear, especially for newcomers. E.g. the plugin in one case is actually a runtime middleware, and in another — a build-time development extension.
I was exploring further and even thought to implement my own Tailwind V4 plugin, but... good news, there is already https://github.com/pakornv/fresh-tailwindcss-v4. @pakornv already managed to build an alpha version for Fresh 2. Just tried it locally — worked well with daisyui 🚀.
"nodeModulesDir": "auto",
"imports": {
"@pakornv/fresh-plugin-tailwindcss": "jsr:@pakornv/[email protected]",
"fresh": "jsr:@fresh/core@^2.0.0-alpha.34",
"preact": "npm:preact@^10.26.6",
"@preact/signals": "npm:@preact/signals@^2.0.4",
"tailwindcss": "npm:tailwindcss@^4.1.7"
},
Why it matters because with TW v4 we can use Tailwind Plus (not for free though) that simplifies layout design and components development with its catalyst, etc.
Expressive syntax for page creation
How can I achieve something like this outside of the routes folder?
// some-flow.tsx:
const helloPage = define.page(() => {
return (
<div>
<h1>Hello!</h1>
<FlowForm /> // imported component
</div>
);
});
app.get("/flow", () => helloPage);
Is this supported, encouraged in Fresh 2?
Right now this is not supported outside of the routes/ folder. There is an existing issue for making the concepts of layouts and routes more general instead of being locked away behind a plugin. See https://github.com/denoland/fresh/issues/2793
Right now if I want to:
- Add security headers → Custom middleware
- Handle authentication flows → Custom implementation
- Log HTTP requests → Custom solution
- Handle file uploads → Figure it out from scratch
- Validate request data → Build from ground up
In Express/Koa, these are solved problems with battle-tested middleware.
Maybe I'm missing something, but it seems like Fresh 2 would benefit from either:
- Making it easy to adapt existing patterns from Express/Koa community;
- Building its own ecosystem of common middleware (like
@fresh/security,@fresh/logging, etc.);- Clear guidance on how to handle these common cases and how to adopt existing utilities.
The middleware in Hono is almost compatible with the one in Fresh, given that they both use Promise<Response> as the return value for Middleware. It should likely be possible to create a wrapper for using most Hono Middleware without problems:
import { toHono } from 'fresh/hono'; // imaginary added API
import { compress } from 'hono/compress';
app.use(toHono(compress()))
Much of the more modern frameworks has landed on using Request/Response as base primitives for middleware as input and return value. So it should be possible re-use many of them as long as FreshContext can be mapped to Context from Hono or other frameworks. Or it can at least serve as a base to create custom plugins.
I think Fresh might come with common middleware (e.g. CORS, etc) eventually as long as there isn't some easy alternative to use instead.
What happens in this case?
app.use(define.middleware((ctx) => {
ctx.state.theme = "light";
return ctx.next();
}));
app.get("/flow", (ctx) => {
return ctx.render(
<AppHtml>
<LandingPage />
</AppHtml>,
);
});
Will I have FreshContext with relevant state in the components by default? Or, do I need to pass ctx via props?
UPD:
Just learned that one option is to use useContext to avoid props drilling. Like so:
export const AppContext = createContext({theme: "light"}) // somewhere
// Use anywhere
import { AppContext} from "./overtherainbow.ts";
const theme = useContext(AppContext);
Still learning, yeah. This particular pattern I learned from Hono: https://hono.dev/docs/guides/jsx#context
I feel like if you want people to test the Alpha, some documentation would be extremely beneficial. Any chance of getting the docs out soon?
Will Fresh 2 use a components/ folder?
When I run deno run -Ar jsr:@fresh/[email protected] I get a the components/ folder but the docs don't say there is one. And I don't see anything about components in the canary docs.
@crowlsyong There is nothing special about the components/ folder from Fresh's point of view. It could also have been named foobar or stuff. We can add a line to the docs saying that this is a folder with no special meaning.
Copy that.
I've been using the components/ folder to nest islands, like this:
// components/ExampleComponent.tsx
import InteractiveThing from "../islands/InteractiveThing.tsx";
export default function ExampleComponent() {
return (
<main>
<InteractiveThing />
</main>
);
};
// islands/InteractiveThing.tsx
import { useState } from "preact/hooks";
export default function InteractiveThing() {
const [count, setCount] = useState(0);
return (
<div>
<p>The current count is: {count}</p>
<button type="button" onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Is that appropriate?
Sure, that's perfectly fine.
Closing this particular issue as there aren't further actionable steps to do. There is documentation about the alpha (now beta) here https://fresh.deno.dev/docs/canary/introduction