Notes on Remix 3

← All notes

There is one rule in Remix 3 that you will type wrong on your first afternoon and never type wrong again — there is no top-level import, every package is a subpath, and the whole framework gets quieter the moment you accept it

by A Developer ·  May 26, 2026  ·  5 min read

`import { ... } from "remix"` does not work. There is no barrel. Everything you load comes from `remix/<something>` — and once that lands, the framework stops feeling like an SDK and starts feeling like a standard library.

The first thing that breaks in Remix 3 is muscle memory. You sit down, open a file, start typing import { something } from 'remix' the way you’ve done in twenty other projects — and nothing autocompletes.

There’s a reason for that. The string 'remix' isn’t a valid import. There is no top-level entry to the package. Everything you load comes from a subpath:

import { createRouter } from 'remix/router'
import { route, get } from 'remix/routes'
import { redirect } from 'remix/response/redirect'
import { compression } from 'remix/middleware/compression'
import { session } from 'remix/middleware/session'
import { Database } from 'remix/data-table'
import { css, type Handle } from 'remix/ui'

Looking at it the first time, this feels like noise. Six imports to boot a router? Six imports later, you start to notice something. Every import is a sentence. You can read the top of any Remix 3 file and know exactly what it touches before you’ve read a single line of the body.

It’s the same trick the standard library uses

Node has it. Python has it. Go has it. You import node:fs/promises, not node. You import collections.abc, not collections. You import net/http, not net.

Subpath imports tell the reader what surface area a module is using without making them open the file. They make grep work. They make code review work. They make AI agents pick the right package, because the package name describes itself.

It’s also how the framework stays small

A single barrel export forces every consumer to pay for everything in the barrel. Tree-shaking helps a little, but only after the bundler has loaded and parsed the whole graph. With subpath imports, the bundler only sees what you asked for.

That matters more than it sounds like it should. The whole Remix 3 package ships everything from auth schemes to a tar parser to a terminal-styling library. If you imported from 'remix', even with perfect tree-shaking, your tooling would have to load the type definitions for all of it just to give you autocomplete. With subpaths, your editor opens one file when you import one thing.

One rule, two consequences

The rule is: import from remix/<subpath>, never from 'remix'. There’s no escape hatch. There’s no barrel hiding somewhere for the lazy. The bundler will complain. The TypeScript compiler will complain. The Remix doctor will complain.

The two consequences, once you internalize it:

  1. Files self-document. The imports at the top tell you the file talks to the router, or the session, or the database, or none of the above.
  2. Refactors get cheap. Moving a piece of code from one layer to another is usually a one-line change at the import site, not a search across the codebase for transitive uses.

The package map is the docs

When the convention is that each subpath names what it does, the table of contents writes itself. The README in node_modules/remix is mostly a list of subpaths with one sentence each:

  • remix/router — the router itself
  • remix/routes — declarative route builders
  • remix/response/redirectredirect(href, status?)
  • remix/session — per-browser state
  • remix/cookie — plain signed and unsigned cookies
  • remix/data-schema — schema builders for runtime validation
  • remix/ui — the component runtime
  • remix/file-storage/s3 — S3-backed file storage

Once you’ve seen the map, you stop searching. You know that anything about HTTP responses lives under remix/response/*. You know that anything about parsing input lives under remix/data-schema/*. The subpath is the search result.

The trade

You type more characters. That’s the whole cost. Six imports to wire up a Remix app instead of one, on a code style where every line is already informative — it’s a deal worth taking.

And there is a quiet payoff that takes a week to notice: you stop looking things up. The shape of the import tells you what the function does. The package name tells you which layer it lives in. The file becomes its own documentation, and you can read someone else’s Remix 3 codebase faster than you can read your own React one.