Client/Server Interface

Dated Apr 19, 2024; last modified on Fri, 19 Apr 2024

How to handle redirects without setting window.location.href?

Right now, there’s a pattern of doing:

sendHTTPRequest("POST", "/login/", {})
  .then((_) => {
      window.location.href = "/";
  })
  .catch((err) => { console.error(err); });

Isn’t this something that the server can do? In response, why not issue a redirect?

Screenshot of the redirect chain from /login. The POST
request gets a 303 (See Other) redirect to /home. The browser then
makes a GET request to /home, which results in a 304 (Not Modified).
Why doesn’t the browser navigate me to /home?

Screenshot of the redirect chain from /login. The POST request gets a 303 (See Other) redirect to /home. The browser then makes a GET request to /home, which results in a 304 (Not Modified). Why doesn’t the browser navigate me to /home?

The 304 Not Modified response is sent when there is no need to retransmit the requested resources. In this case, the page at /home has not changed, and the browser can use the cached version. If the content had changed, then the browser would have requested the resource from the server, resulting in a 200 response.

Installing http-status-codes for more legible status codes.

This issue does not happen when I issue a GET request to /logout by clicking on a <a href="/logout">Log Out</a>. The server responds with a 303 (See Other) to /browse, and the browser loads /browse in the tab. If I do the same with fetch('/logout', { method: 'GET' }) instead, then the browser does not navigate the page to /browse despite the /browse response being observable in the Network tab. The consensus online is that fetch responses do not lead to automatic browser navigation.

Re: fetch vs. XMLHttpRequest. The latter is the older way of making HTTP requests from JavaScript without leaving the page; it is based on events. fetch is built around promises, which are considered the preferred way of doing asynchronous operations. fetch and XMLHttpRequest are implemented by browser vendors. On top of these, numerous libraries exist, e.g., Fetch polyfill, isomorphic-fetch, axios, jQuery, etc., to fill various gaps, e.g., different syntax, varying browser support, etc.

Typing the Client/Server Interface

One source of bugs is the server and the client being out of sync w.r.t. the shape of the data being exchanged. Granted, I have both the server and the client in the same repo, how can I avoid mismatches in data? Is there a library or a standard way of creating an interface for my api endpoints? : typescript floats tRPC, GraphQL, OpenAPI, and some others, with tRPC coming in first and GraphQL as the incumbent.

GitHub Copilot can help out here with quick intros on what tRPC and GraphQL entail. With GraphQL, the client can strongly type the API, define the data payloads, fetch all the data in one request, query for supported types, and subscribe to real-time updates. GraphQL is intended to be an improvement over REST. tRPC is designed with TypeScript in mind. Advantages of tRPC include no schema syncing as the input/output is inferred directly from function signatures, and can work with GraphQL (or any other data fetching method). In the case of my app, using tRPC involves replacing Express routes with tRPC procedures. Trying tRPC!

Impressed by Copilot. Definitely saved me time on this one.

What’s the use case for tRPC for endpoints that return a static page in response to a GET request? Copilot says that serving HTML files should be left to traditional web frameworks like Express as those have built-in support for SSR of HTML.

Adding tRPC endpoints that don’t need authentication was mostly mechanical. However, there were a few gotchas. Some methods/props on a Mongoose document (e.g., the Document interface) cannot be shared between the server and the client, and so we need to define a safe interface that can be inferred by tRPC and availed on the client side. We also encountered a challenge in typing the inputs, given that the app relies on Mongoose validation that occurs after tRPC has seen the inputs. Ended up adding a passthrough input parser, e.g.,

export const authRouter = router({
  registerUser: publicProcedure
    .input((params: unknown) => params as RegisterUserAndPasswordParams)
    .mutation(({ input }) => {
      return registerUserAndPassword(input);
    }),
});

The docs are not explicit about this because not validating is considered unsafe, and the tRPC maintainers don’t want to advocate for it. With the base knowledge from non-auth endpoints and reading the docs several times, adding auth-dependent endpoints integrated nicely with Express’s session management.

References

  1. 304 Not Modified - HTTP | MDN. developer.mozilla.org . Accessed Apr 19, 2024.
  2. javascript - Difference between fetch, ajax, and xhr - Stack Overflow. stackoverflow.com . Accessed Apr 19, 2024.
  3. AJAX/HTTP Library Comparison. www.javascriptstuff.com . Feb 3, 2016. Accessed Apr 19, 2024.
  4. [Next.JS] x.data.user is of type 'unknown' · trpc/trpc · Discussion #3661. github.com . Accessed Apr 20, 2024.
  5. docs: Howto add typed input (without validation) · Issue #3339 · trpc/trpc. github.com . Accessed Apr 20, 2024.