The App Router was beta, and I treated it that way

Next.js 13 made the new app directory compelling in October 2022; P6X4 evaluated it as beta infrastructure instead of rewriting its stable routes on announcement day.

Next.js 13 arrived with a new app directory, layouts, React Server Components, streaming, and a new data-fetching model. It was the clearest public shape yet for ideas I had been following since the React Server Components research announcement.

It was also labelled beta.

P6X4 had just spent months removing boundaries adopted before their value was clear. Rebuilding every route around an exciting beta architecture would have repeated the same habit at the framework layer.

I made a small, disposable evaluation and kept the existing pages stable.

The release contained two decisions

The announcement combined a stable major framework release with a beta routing architecture. Treating “Next.js 13” as one maturity level would have blurred that distinction.

The existing pages directory remained the supported route model for P6X4's established interface. The app directory preview offered a place to test nested layouts, server-first components, Suspense integration, and the new conventions. I recorded the latter as an experiment, not a migration baseline.

This allowed ordinary dependency work and the architectural evaluation to move independently. If the experiment failed, P6X4 did not need to roll back its whole framework version.

The note sounds obvious in retrospect. Release excitement makes maturity labels surprisingly easy to reinterpret as encouragement rather than constraint.

The experiment used a document evidence view with synthetic data. It had stable identity, a primary revision, slower history, and an optional related-documents section. It was ideal for studying server rendering and nested loading boundaries without putting a consequential mutation behind an unfamiliar model.

I did not begin with delivery, account settings, authentication callbacks, or the write-fence migration interface. Those routes carried durable effects, authorization, or recovery behavior I did not want to change while learning the framework.

The evaluation lived under an internal path and was excluded from the public navigation. It could be removed without migrating content or preserving URLs.

This was progressive adoption applied to learning: use a representative constraint, but keep the blast radius reversible.

Layouts solved a real composition problem

P6X4's existing route wrapper loaded document identity, navigation, and access context in several pages. Nested layouts suggested a cleaner relationship: a document segment could establish stable context while child routes rendered overview, evidence, or delivery details.

The prototype made the hierarchy legible:

app/
  layout.tsx
  documents/
    [documentId]/
      layout.tsx
      page.tsx
      evidence/
        page.tsx

I tested navigation between child pages and observed which UI and data persisted. The layout reduced repeated composition, but it also raised questions:

  • Which authorization decisions could be cached or reused?
  • What happened when a document became inaccessible during navigation?
  • Which errors should replace the child versus the document frame?
  • How would route-level loading states interact with nested Suspense boundaries?

The file structure made a design opportunity visible. It did not answer those product questions automatically.

The prototype rendered most document content on the server and kept interactive filtering or copy actions in smaller client components. That reduced the amount of component code and data shaping sent to the browser.

The boundary required discipline. A Server Component could load data close to authority and pass serializable results to a client component. It could not hand over a database repository, callback closure, or arbitrary class instance. A client component introduced a client subtree and its dependencies into the browser build.

I marked client boundaries deliberately and inspected the resulting bundle. One convenience component imported a formatting library and pulled more code client-side than the interaction justified. Moving formatting back to the server and passing the display value kept the client island small.

This was a useful design pressure. It also meant existing components written under a client-everywhere assumption were not drop-in building blocks.

Data fetching needed new mental models

The experiment fetched document data directly in async server components according to the beta model. I tested repeated reads in a layout and page, cache behavior, request identity, and how the route handled a source revision changing.

P6X4's domain layer already separated authoritative queries from projections. I kept that API rather than letting route components query tables freely. The component received a supported document view and revision metadata.

Caching was the area I treated most cautiously. Document evidence had different freshness needs from navigation labels and static help text. I recorded every route's authority, acceptable staleness, and invalidation trigger before choosing a cache policy.

The beta surface was evolving, so I avoided building a custom abstraction that pretended the final conventions were known. The evaluation contained framework-specific code at its edge and stable domain contracts beneath it.

The new route model and React 18 integration made streaming with Suspense natural to explore. My first boundary wrapped every data request. The page arrived early and assembled itself in distracting fragments.

I regrouped boundaries around user concepts: document identity in the initial frame, evidence history as one secondary region, and related documents as optional. Stable headings and geometry arrived before details. An error in related results did not replace the document.

The important question was not “Can this component stream?” It was “Can this product region arrive later without confusing what the reader already knows?”

That lesson belonged to P6X4 regardless of whether the app directory became its future routing system.

Loading files were powerful and broad

A segment-level loading file could provide an immediate state during navigation. In the prototype, a generic fallback briefly replaced content even when a nested query was the only slow part. The route felt more active and less stable.

I compared route loading with component-level Suspense. Segment loading was useful when the whole child route lacked enough information to render. A narrower boundary was better when document identity and navigation could persist.

I also checked focus and announcements. Replacing the main heading during every navigation made the structure harder to follow. Keeping the document frame stable and updating one labelled region produced a calmer experience.

The convention reduced setup. It did not remove the need to choose recovery and comprehension boundaries.

The route experiment included error and not-found behavior. I forced missing documents, lost authorization, projection failure, malformed revision IDs, and unexpected rendering exceptions.

A missing document and an inaccessible document deliberately produced the same public absence to avoid revealing existence. A failed optional projection stayed local. A malformed route parameter failed before querying. An unexpected document-authority failure replaced the document region and logged the stable request context.

Resetting an error boundary was safe only for read work. A mutation with uncertain outcome could not be retried through a generic reset button.

The framework supplied a place to handle errors. P6X4 still had to decide what remained trustworthy and which actions were safe.

Metadata and head behavior needed observation

The document prototype generated a title and description from authoritative read data. I checked initial HTML, navigation updates, missing states, and whether private details could leak into metadata generated before authorization.

The safe order was authorization first, then bounded metadata. Generic absence metadata covered missing or inaccessible records. I did not use projection results for the canonical document identity.

This test revealed another reason not to migrate blindly. Metadata APIs and conventions were part of the new routing model, and incorrect behavior could remain invisible in the rendered body.

The experiment recorded expected HTML and head output as contract fixtures rather than relying on a visual check alone.

Incremental adoption was attractive, but two routing systems introduce their own coordination:

  • Shared navigation needed to know which transitions crossed router boundaries.
  • Design primitives had to work under both rendering models.
  • Authentication and request context could not diverge.
  • Error and analytics conventions needed comparable semantics.
  • Duplicate URLs and metadata had to be prevented.

I made the evaluation route visibly internal and did not create competing public URLs. Shared domain and design packages remained framework-light. The experiment imported them through supported entry points.

Coexistence was an evaluation mechanism, not permission to maintain two permanent applications.

I wrote adoption gates before enthusiasm changed them

The experiment had explicit gates:

  1. The routing architecture reaches stable status for the features P6X4 needs.
  2. Authentication, metadata, error, and deployment behavior have documented supported paths.
  3. A representative route passes performance, accessibility, and failure scenarios.
  4. The client bundle and server work are measured against the existing route.
  5. Incremental migration preserves URLs and one authority model.
  6. A removal path exists for the old route.

I also wrote rejection conditions: unclear cache semantics for consequential reads, an unstable API requiring broad wrappers, regressions in keyboard navigation or recovery, and no meaningful benefit for the route.

The document prevented a polished demonstration from becoming the decision by default.

I compared:

  • Server response and first useful document identity.
  • JavaScript transferred for the route.
  • Time until interactive controls actually responded.
  • Layout movement as streamed regions arrived.
  • Navigation continuity between nested pages.
  • Behavior with JavaScript delayed.
  • Error containment and log context.
  • Complexity of domain access and tests.

The prototype sent less client code and produced a compelling server-first structure. It also required new test harness work and careful understanding of beta cache and routing behavior.

I did not compress those findings into one winner score. The benefits and adoption costs belonged to different dimensions.

I avoided future terminology

The October 2022 note described what existed then: the beta app directory, layouts, Server Components, streaming, and the accompanying data-fetching direction. I did not write as though later stable conventions, APIs, or production guidance had already arrived.

This mattered for the integrity of the archive. A release reaction should contain contemporary uncertainty. Retrofitting a mature framework story into the announcement day would make the judgment look easier than it was.

The prototype remained a dated branch with its tested assumptions. Later work could revisit it using later documentation and compare what changed.

Treating the App Router as beta did not mean dismissing it. The experiment changed how I thought about client boundaries, nested layouts, streaming, and server-first composition. Those lessons influenced P6X4's interface even while stable routes stayed on the existing system.

I could adopt ideas before adopting infrastructure. Stable document regions became clearer. Client components became smaller. Loading boundaries followed comprehension. Domain APIs stopped assuming all consumers ran in the browser.

The restraint was architectural, not emotional. I was excited by the direction and unwilling to convert that excitement into migration risk without evidence.

P6X4 had taught me that a boundary should earn its cost. A framework rewrite deserved the same standard.

The App Router was beta, and I treated the label as part of the API.

The read-only experiment made it tempting to add a small “rename document” form. I declined because the mutation path would have mixed three investigations: the beta router, server-first rendering, and P6X4's accepted-intent protocol.

The existing route posted a stable intent ID, expected document revision, payload digest, and authority context to a supported endpoint. It handled accepted, conflicted, unknown, and invalid outcomes. Replacing it required more than finding a convenient way to run server code from a component.

I instead rendered a link back to the established edit route and tested that navigation preserved document identity. This looked inelegant in the prototype. It kept a durable workflow on the path whose semantics and recovery I understood.

The decision reinforced a boundary I would reuse later: a new rendering model may change how a form is delivered, but it does not get to erase the command protocol behind the form.

Authentication was evaluated at every rendering point

Server rendering can move data access closer to components, which makes authorization easy to scatter. I kept one request-context boundary that resolved session, subject, policy revision, and disclosure-safe absence behavior.

The layout loaded only the identity needed to establish the document frame. Child components requested supported views through that context. Optional regions could not bypass it by calling a repository directly. Client components received minimal display data rather than raw session objects or policy claims.

I tested direct requests, client navigation, expired sessions, permission removal between navigations, and slow authorization. The server-first model reduced client exposure, but only because access checks remained deliberate. Running code on the server was not itself authorization.

Cookie and middleware behavior were areas I recorded as framework integration assumptions subject to change while the router was beta. I avoided a wrapper intended to hide every uncertainty; it would have fossilized the experimental surface in my own abstraction.

The prototype ran in P6X4's local Node-style environment. I examined which routes were evaluated at build time, request time, or through cached results and how that matched private document data. A page that accidentally became static could preserve one synthetic record beyond its intended authority context.

I asserted response headers and rendered output for authenticated and unauthenticated requests. The build report was inspected for route classification. The experiment failed closed when required runtime configuration was absent.

I also tested streaming through the local reverse proxy, including buffering behavior. A server can produce chunks that an intermediary holds until the response is complete. The product benefit depended on the complete path, not the component API alone.

Deployment compatibility therefore sat in the adoption gates beside developer experience. A route architecture is partly a runtime contract.

The experiment had a deletion date

The branch included a short manifest: questions, source release, beta APIs touched, scenarios, observed results, and a date to revisit or delete. Generated fixtures contained no real personal data, and the route was unavailable outside the lab.

Without an end condition, experiments become accidental dependencies. Other code begins importing their helpers, test infrastructure assumes their paths, and removal feels like losing progress.

I prevented production packages from depending on the experiment and copied back only stable lessons: smaller client islands, clearer semantic loading regions, and framework-independent domain queries. The prototype itself remained expendable.

When the evaluation window ended, I archived its decision record and removed the running route. Later framework work would begin from current documentation, not from a beta branch treated as timeless.

The usual story frames waiting as missed momentum. In this case, waiting let the routing model, framework documentation, and integration practices evolve while P6X4 continued to improve on stable infrastructure.

The cost was that I did not gain broad migration experience immediately. The benefit was avoiding a large compatibility layer around assumptions likely to change. The small prototype preserved enough firsthand knowledge to evaluate later stability without wagering every route.

Restraint was not zero action. It was a bounded experiment plus explicit evidence requirements.

The direction mattered even before adoption

Server-first components challenged a habit I had carried from earlier universal JavaScript experiments: assume components and their dependencies belong in the browser unless proven otherwise. Nested layouts challenged route wrappers that reloaded stable context. Integrated Suspense made the relationship between data boundaries and visual revelation impossible to ignore.

I applied those insights to the existing P6X4 interface. Domain queries returned bounded server-ready views. Client components narrowed to real interaction. Page regions reflected authority and recovery. None required claiming the beta router was already stable infrastructure.

The experiment succeeded because it improved judgment, not because it forced a migration.

I also kept the comparison reproducible. The decision record pinned the framework release, captured the representative route fixture, listed proxy settings, and stored the performance and accessibility scenarios. A future revisit could distinguish framework progress from a faster laptop, warmer cache, or simplified data. The beta prototype was evidence with a date, not a demonstration whose conditions would be forgotten.