Fresh logo

Migration Guide

We tried to keep breaking changes in Fresh 2 as minimal as possible, but some changes need to be updated manually. Fresh 2 comes with many quality of life improvements that make it easier to extend and adapt Fresh. We’ve created this upgrade guide as part of upgrading our own apps here at Deno.

Use this guide to migrate a Fresh 1.x app to Fresh 2.

Applying automatic updates

Most changes can be applied automatically with the update script. Start the update by running it in your project directory:

Terminal (Shell/Bash) Terminal
deno run -Ar jsr:@fresh/update

This will apply most API changes made in Fresh 2 automatically update like changing $fresh/server.ts imports to fresh.

Getting main.ts and dev.ts ready

Configuring Fresh doesn’t require a dedicated config file anymore. You can delete the fresh.config.ts file. The fresh.gen.ts manifest file isn’t needed anymore either.

File diff Project structure
  <project root>
  ├── routes/
- ├── dev.ts
- ├── fresh.gen.ts
- ├── fresh.config.ts
  └── main.ts

Fresh 2 takes great care in ensuring that code that’s only needed during development is separate from production code. This split makes deployments much smaller, quicker to upload and allows them to boot up much quicker in production.

Replacing dev.ts with vite.config.ts

Delete dev.ts and create a vite.config.ts file instead. Pass your custom Fresh configuration to the fresh vite plugin.

Typescript vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [
    fresh(),
    tailwindcss(),
  ],
});

Add a client.ts file

The client.ts file is the main entry file for client-side code. Since vite requires everything to be part of the internal module graph to get hot module reloading to work, this is also the place where you import CSS assets.

File diff Project structure
  <project root>
  ├── routes/
+ ├── client.ts
  ├── vite.config.ts
  └── main.ts
Typescript client.ts
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";

Updating main.ts

Similarly, configuration related to running Fresh in production can be passed to new App():

Typescript main.ts
import { App, staticFiles } from "fresh";

export const app = new App()
  // Add static file serving middleware
  .use(staticFiles())
  // Enable file-system based routing
  .fsRoutes();

Merging error pages

Both the _500.tsx and _404.tsx template have been unified into a single _error.tsx template.

File diff Project structure
  └── <root>/routes/
-     ├── _404.tsx
-     ├── _500.tsx
+     ├── _error.tsx
      └── ...

Inside the _error.tsx template you can show different content based on errors or status codes with the following code:

Typescript routes/_error.tsx
export default function ErrorPage(props: PageProps) {
  const error = props.error; // Contains the thrown Error or HTTPError
  if (error instanceof HttpError) {
    const status = error.status; // HTTP status code

    // Render a 404 not found page
    if (status === 404) {
      return <h1>404 - Page not found</h1>;
    }
  }

  return <h1>Oh no...</h1>;
}

Updating tasks

The server entrypoint is now generated by Fresh for more optimal startup times. This means you need to update your task when launching Fresh in production mode.

To launch Fresh in production mode:

File diff  
- deno run -A main.ts
+ deno serve -A _fresh/server.js

You’ll likely have that command inside your deno.json as a task. Update it accordingly.

JSON deno.json
  {
    "tasks":
-     "dev": "deno run -A dev.ts",
-     "build": "deno run -A dev.ts build",
-     "preview": "deno run -A main.ts"
+     "dev": "vite",
+     "build": "vite build",
+     "preview": "deno serve -A _fresh/server.js"
   }
  }

Update deployment settings

Fresh 2 requires assets to be build during deployment instead of building them on demand. Run the deno task build command as part of your deployment process. If you have already set up Fresh’s 1.x “Ahead-of-Time Builds”, then no changes are necessary.

Trailing slash handling

The handling trailing slashes has been extracted to an optional middleware that you can add if needed. This middleware can be used to ensure that URLs always have a trailing slash at the end or that they will never have one.

Typescript main.ts
-  import { App, staticFiles } from "fresh";
+  import { App, staticFiles, trailingSlashes } from "fresh";

  export const app = new App({ root: import.meta.url })
    .use(staticFiles())
+   .use(trailingSlashes("never"));

Automatic updates

Info

The changes listed here are applied automatically when running the @fresh/update script and you shouldn’t need to have to do these yourself.

Unified middleware signatures

Middleware, handler and route component signatures have been unified to all look the same. Instead of receiving two arguments, they receive one. The Request object is stored on the context object as ctx.req.

Typescript middleware.ts
- const middleware = (req, ctx) => new Response("ok");
+ const middleware = (ctx) => new Response("ok");

Same is true for handlers:

Typescript route/page.tsx
  export const handler = {
-   GET(req, ctx) {
+   GET(ctx) {
      return new Response("ok");
    },
  };

…and async route components:

Typescript routes/my-page.tsx
-  export default async function MyPage(req: Request, ctx: RouteContext) {
+  export default async function MyPage(props: PageProps) {
    const value = await loadFooValue();
    return <p>foo is: {value}</p>;
  }

All the various context interfaces have been consolidated and simplified:

Fresh 1.x Fresh 2.x
AppContext, LayoutContext, RouteContext Context

Context methods

The ctx.renderNotFound() method has been removed in favor of throwing an HttpError instance. This allows all middlewares to optionally participate in error handling. Other properties have been moved or renamed to make it easier to re-use existing objects internally as a minor performance optimization.

Fresh 1.x Fresh 2.x
ctx.renderNotFound() throw new HttpError(404)
ctx.basePath ctx.config.basePath
ctx.remoteAddr ctx.info.remoteAddr

ctx.render()

The meaning of ctx.render() has changed. In Fresh 1.x this was used to pass data from a handler to a component. In Fresh 2.x this function has been generalized to render JSX.

Fresh 1.x:

Typescript  
export const handlers = {
  async GET(req, ctx) {
    const data = await Query();
    await ctx.render({ value: data });
  },
};

export default function Page({ data }) {
  // ...
}

Fresh 2:

Typescript  
export const handlers = {
  async GET(ctx) {
    const data = await Query();
    return { data: { value: data } };
  },
};

export default function Page({ data }) {
  // ...
}

To render JSX in general, use the ctx.render() function:

Typescript  
const app = new App()
  .get("/", () => ctx.render(<h1>hello</h1>));

createHandler

The createHandler function was often used to launch Fresh for tests. This can be now done via vite’s createBuilder function. See the testing page for more information.

Getting help

If you run into problems with upgrading your app, reach out to us by creating an issue here https://github.com/denoland/fresh/issues/new . That way we can improve this migration guide for everyone.