{"id":7452,"date":"2025-10-24T13:59:02","date_gmt":"2025-10-24T18:59:02","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7452"},"modified":"2025-10-24T13:59:02","modified_gmt":"2025-10-24T18:59:02","slug":"introducing-tanstack-start-middleware","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/introducing-tanstack-start-middleware\/","title":{"rendered":"Introducing TanStack Start Middleware"},"content":{"rendered":"\n<p><a href=\"https:\/\/tanstack.com\/start\/latest\">TanStack Start<\/a> is one of the most exciting full-stack web development frameworks I&#8217;ve seen. <a href=\"https:\/\/frontendmasters.com\/blog\/introducing-tanstack-start\/\">I&#8217;ve written about it before.<\/a><\/p>\n\n\n\n<p>In essence, TanStack Start takes <a href=\"https:\/\/tanstack.com\/router\/latest\">TanStack Router<\/a>, a superb, strongly-typed client-side JavaScript framework, and adds server-side support. This serves two purposes: it gives you a place to execute server-side code, like database access; and it enables server-side rendering, or SSR.<\/p>\n\n\n\n<p>This post is all about one particular, especially powerful feature of TanStack Start: <strong>Middleware<\/strong>. <\/p>\n\n\n\n<p>The elevator pitch for Middleware is that it allows you to execute code in conjunction with your server-side operations, executing code on both the client and the server, both before and after your underlying server-side action, and even passing data between the client and server.<\/p>\n\n\n\n<p>This post will be a gentle introduction to Middleware. We&#8217;ll build some <em>very<\/em> rudimentary observability for a toy app. Then, in a future post, we&#8217;ll really see what Middleware can do when we use it to achieve single-flight mutations.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"why-do-we-need-ssr\">Why SSR?<\/h2>\n\n\n\n<p>SSR will usually improve LCP (Largest Contentful Paint) render performance compared to a client-rendered SPA. With SPAs, the server usually sends down an empty shell of a page. The browser then parses the script files, and fetches your application components. Those components then render and, usually, <em>request some data<\/em>. Only <em>then<\/em> can you render actual content for your user.<\/p>\n\n\n\n<p>These round trips are neither free nor cheap; SSR allows you to send the initial content down directly, via the <em>initial<\/em> request, which the user can see <em>immediately<\/em>, without needing those extra round trips. See the post above for some deeper details; this post is all about Middleware.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prelude-server-functions\">Prelude: Server Functions<\/h2>\n\n\n\n<p>Any full-stack web application will need a place to execute code on the server. It could be for a database query, to update data, or to validate a user against your authentication solution. Server functions are the main mechanism TanStack Start provides for this purpose, and are documented <a href=\"https:\/\/tanstack.com\/start\/latest\/docs\/framework\/react\/server-functions\">here<\/a>. The quick introduction is that you can write code like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { createServerFn } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getServerTime = createServerFn().handler(<span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span>));\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then you can call that function from <em>anywhere<\/em> (client or server), to get a value computed on the server. If you call it from the server, it will just execute the code. If you call that function from the browser, TanStack will handle making a network request to an internal URL containing that server function.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"getting-started\">Getting Started<\/h2>\n\n\n\n<p>All of my prior posts on TanStack Start and Router used the same contrived Jira clone, and this one will be no different. <a href=\"https:\/\/github.com\/arackaf\/tanstack-start-middleware-blog-post\">The repo is&nbsp;here<\/a>, but the underlying code is the same. If you want to follow along, you can&nbsp;<code>npm i<\/code>&nbsp;and then&nbsp;<code>npm run dev<\/code>&nbsp;and then run the relevant portion of the app at&nbsp;<a href=\"http:\/\/localhost:3000\/app\/epics?page=1\">http:\/\/localhost:3000\/app\/epics?page=1<\/a>.<\/p>\n\n\n\n<p>The epics section of this app uses server functions for all data and updates. We have an overview showing:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A count of all tasks associated with each individual epic (for those that contain tasks).<\/li>\n\n\n\n<li>A total count of all epics in the system.<\/li>\n\n\n\n<li>A pageable list of individual epics which the user can view and edit.<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"715\" height=\"1024\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3-1.png?resize=715%2C1024&#038;ssl=1\" alt=\"A web application displaying an epics overview with a list of projects, their completion status, and navigation buttons.\" class=\"wp-image-7458\" style=\"width:355px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3-1.png?resize=715%2C1024&amp;ssl=1 715w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3-1.png?resize=209%2C300&amp;ssl=1 209w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3-1.png?resize=768%2C1100&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img3-1.png?w=878&amp;ssl=1 878w\" sizes=\"auto, (max-width: 715px) 100vw, 715px\" \/><figcaption class=\"wp-element-caption\">This is a contrived example. It&#8217;s just to give us a few different data sources along with mutations.<\/figcaption><\/figure>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"our-middleware-use-case\">Our Middleware Use Case<\/h2>\n\n\n\n<p>We&#8217;ll explore middleware by building a rudimentary observability system for our Jira-like app.<\/p>\n\n\n\n<p>What is observability? If you think of basic logging as a caterpillar, observability would be the beautiful butterfly it matures into. Observability is about setting up systems that allow you to holistically observe how your application is behaving. High-level actions are assigned a globally unique trace id, and the pieces of work that action performs are logged against that same trace id. Then your observability system will allow you to intelligently introspect that data, and discover where your problems or weaknesses are.<\/p>\n\n\n\n<p>I&#8217;m no observability expert, so if you&#8217;d like to learn more, Charity Majors <a href=\"https:\/\/www.amazon.com\/Observability-Engineering-Achieving-Production-Excellence\/dp\/1492076449\">co-authored a superb book on this very topic<\/a>. She&#8217;s the co-founder of <a href=\"https:\/\/www.honeycomb.io\/\">Honeycomb IO<\/a>, a mature observability platform.<\/p>\n\n\n\n<p>We won&#8217;t be building a mature observability platform here; we&#8217;ll be putting together some rudimentary logging with trace id&#8217;s. What we&#8217;ll be building is not suitable for use in a production software system, but it <em>will<\/em> be a great way to explore TanStack Start&#8217;s Middleware.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"our-first-server-function\">Our First Server Function<\/h2>\n\n\n\n<p>This is a post about Middleware, which is applied to server functions. Let&#8217;s take a very quick look at a server function<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getEpicsList = createServerFn({ <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">\"GET\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\">page: number<\/span>) =&gt;<\/span> page)\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> epics = <span class=\"hljs-keyword\">await<\/span> db\n      .select()\n      .from(epicsTable)\n      .offset((data - <span class=\"hljs-number\">1<\/span>) * <span class=\"hljs-number\">4<\/span>)\n      .limit(<span class=\"hljs-number\">4<\/span>);\n    <span class=\"hljs-keyword\">return<\/span> epics;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This is a simple server function to query our epics. We configure it to use the GET http verb. We specify and potentially validate our input, and then the handler function runs our actual code, which is just a basic query against our SQLite database. This particular code uses <a href=\"https:\/\/orm.drizzle.team\/\">Drizzle<\/a> for the data access, but you can of course use whatever you want.<\/p>\n\n\n\n<p>Server functions by definition always run on the server, so you can do things like connect to a database, access secrets, etc.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"our-first-middleware\">Our First Middleware<\/h2>\n\n\n\n<p>Let&#8217;s add some empty middleware so we can see what it looks like.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { createMiddleware } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> middlewareDemo = createMiddleware({ <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n  .client(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"client before\"<\/span>);\n\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      <span class=\"hljs-attr\">sendContext<\/span>: {\n        <span class=\"hljs-attr\">hello<\/span>: <span class=\"hljs-string\">\"world\"<\/span>,\n      },\n    });\n\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"client after\"<\/span>, result.context);\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  })\n  .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"server before\"<\/span>, context);\n\n    <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span>));\n\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      <span class=\"hljs-attr\">sendContext<\/span>: {\n        <span class=\"hljs-attr\">value<\/span>: <span class=\"hljs-number\">12<\/span>,\n      },\n    });\n\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"server after\"<\/span>, context);\n\n    <span class=\"hljs-keyword\">return<\/span> result;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Let&#8217;s step through it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> middlewareDemo = createMiddleware({ <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This declares the middleware. <code>type: \"function\"<\/code> means that this middleware is intended to run against server &#8220;functions&#8221; &#8211; there&#8217;s also &#8220;request&#8221; middleware, which can run against either server functions, or server routes (server routes are what other frameworks sometimes call &#8220;API routes&#8221;). But &#8220;function&#8221; middleware has some additional powers, which is why we&#8217;re using them here.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">.client(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows us to run code on the client. Note the arguments: <code>next<\/code> is how we tell TanStack to proceed with the rest of the middlewares in our chain, as well as the underlying server function this middleware is attached to. And <code>context<\/code> holds the mutable &#8220;context&#8221; of the middleware chain.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"client before\"<\/span>);\n\n<span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n  <span class=\"hljs-attr\">sendContext<\/span>: {\n    <span class=\"hljs-attr\">hello<\/span>: <span class=\"hljs-string\">\"world\"<\/span>,\n  },\n});\n\n<span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"client after\"<\/span>, result.context);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We do some logging, then tell TanStack to run the underlying server function (as well as any other middlewares we have in the chain), and then, after everything has run, we log again.<\/p>\n\n\n\n<p>Note the <code>sendContext<\/code> we pass into the call to <code>next<\/code><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">sendContext: {\n  <span class=\"hljs-attr\">hello<\/span>: <span class=\"hljs-string\">\"world\"<\/span>,\n},<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows us to pass data from the client, up to the server. Now this <code>hello<\/code> property will be in the context object on the server.<\/p>\n\n\n\n<p>And of course don&#8217;t forget to return the actual result.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You can <code>return next()<\/code>, but separating the call to <code>next<\/code> with the return statement allows you to do additional work after the call chain is finished: modify context, perform logging, etc.<\/p>\n\n\n\n<p>And now we essentially restart the same process on the server.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">  .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"server before\"<\/span>, context);\n\n    <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span>));\n\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n      <span class=\"hljs-attr\">sendContext<\/span>: {\n        <span class=\"hljs-attr\">value<\/span>: <span class=\"hljs-number\">12<\/span>\n      }\n    });\n\n    <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"server after\"<\/span>, context);\n\n    <span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We do some logging and inject an artificial delay of one second to simulate work. Then, as before, we call&nbsp;<code>next()<\/code>&nbsp;which triggers the underlying server function (as well as any <em>other<\/em> Middleware in the chain), and then return the result.<\/p>\n\n\n\n<p>Note again the <code>sendContext<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n  <span class=\"hljs-attr\">sendContext<\/span>: {\n    <span class=\"hljs-attr\">value<\/span>: <span class=\"hljs-number\">12<\/span>,\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows us to send data from the server back down to the client.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"lets-run-it\">Let&#8217;s Run It<\/h3>\n\n\n\n<p>We&#8217;ll add this middleware to the server function we just\u00a0saw.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getEpicsList = createServerFn({ <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">\"GET\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\">page: number<\/span>) =&gt;<\/span> page)\n  .middleware(&#91;middlewareDemo])\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> epics = <span class=\"hljs-keyword\">await<\/span> db\n      .select()\n      .from(epicsTable)\n      .offset((data - <span class=\"hljs-number\">1<\/span>) * <span class=\"hljs-number\">4<\/span>)\n      .limit(<span class=\"hljs-number\">4<\/span>);\n    <span class=\"hljs-keyword\">return<\/span> epics;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When we run it, this is what the\u00a0<em>browser\u2019s<\/em>\u00a0console shows:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">client before<br>client after {value: 12}<\/pre>\n\n\n\n<p>With a one second delay before the final client log, since that was the time execution was on the server with the delay we saw.<\/p>\n\n\n\n<p>Nothing too shocking. The client logs, then sends execution to the server, and then logs again with whatever context came back from the server. Note we use <code>result.context<\/code> to get what the server sent back, rather than the <code>context<\/code> argument that was passed to the <code>client<\/code> callback. This makes sense: that context was created before the server was ever invoked with the <code>next()<\/code> call, so there&#8217;s no way for it to magically, mutably update based on whatever happens to get returned from the server. So we just read <code>result.context<\/code> to get what the server sent back.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-server\">The Server<\/h3>\n\n\n\n<p>Now let&#8217;s see what the server console shows.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">server before { hello: 'world' }<br>server after { hello: 'world' }<\/pre>\n\n\n\n<p>Nothing too interesting here, either. As we can see, the server&#8217;s <code>context<\/code> argument does in fact contain what was sent to it from the client.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"when-client-middleware-runs-on-the-server\">When Client Middleware Runs on the Server<\/h3>\n\n\n\n<p>Don&#8217;t forget, TanStack Start will server render your initial path by default. So what happens when a server function executes as a part of that process, with Middleware? How can the client middleware possibly run, when there&#8217;s no client in existence yet\u2014only a request, currently being executed on the server.<\/p>\n\n\n\n<p>During SSR, client Middleware will run on the server. This makes sense: whatever functionality you&#8217;re building will still work, but the client portion of it will run on the server. So be sure not to use any browser-only APIs like\u00a0<code>localStorage<\/code>.<\/p>\n\n\n\n<p>Let&#8217;s see this in action, but during the SSR run. The prior logs I showed were the result of browsing to a page via navigation. Now I&#8217;ll just refresh that page, and show the <em>server<\/em> logs.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">client before<br>server before { hello: 'world' }<br>server after { hello: 'world' }<br>client after { value: 12 }<\/pre>\n\n\n\n<p>This is the same as before, but now server, and client logs are together, since this code all runs during the server render phase. The server function is called from the server, while it generates the HTML to send down for the initial render. And as before, there&#8217;s a one second delay while the server is working.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"building-real-middleware\">Building Real Middleware<\/h2>\n\n\n\n<p>Let&#8217;s build some actual logging Middleware with an observability flair. If you want to look at real observability solutions, please check out <a href=\"https:\/\/www.amazon.com\/Observability-Engineering-Achieving-Production-Excellence\/dp\/1492076449\">the book<\/a> I mentioned above, or a real Observability solution like <a href=\"https:\/\/www.honeycomb.io\/\">Honeycomb<\/a>. But our focus will be on TanStack Middleware, not a robust observability solution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"the-client\">The Client<\/h3>\n\n\n\n<p>Let&#8217;s start our Middleware with our client section. It will record the local time that this Middleware began. This will allow us to measure the total end-to-end time that our action took, including server latency.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> loggingMiddleware = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span>\n  createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n    .client(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n      <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"middleware for\"<\/span>, name, <span class=\"hljs-string\">\"client\"<\/span>, context);\n\n      <span class=\"hljs-keyword\">const<\/span> clientStart = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now let&#8217;s call the rest of our Middleware chain and our server function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n  <span class=\"hljs-attr\">sendContext<\/span>: {\n    clientStart,\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Once the <code>await next<\/code> completes, we know that everything has finished on the server, and we&#8217;re back on the client. Let&#8217;s grab the date and time that everything finished, as well as a logging id that was sent back from the server. With that in hand, we&#8217;ll call <code>setClientEnd<\/code>, which is just a simple server function to update the relevant row in our log table with the <code>clientEnd<\/code> time.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> clientEnd = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n<span class=\"hljs-keyword\">const<\/span> loggingId = result.context.loggingId;\n\n<span class=\"hljs-keyword\">await<\/span> setClientEnd({ <span class=\"hljs-attr\">data<\/span>: { <span class=\"hljs-attr\">id<\/span>: loggingId, clientEnd } });\n\n<span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>For completeness, that server function looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> setClientEnd = createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">payload<\/span>: { <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">string<\/span>; <span class=\"hljs-params\">clientEnd<\/span>: <span class=\"hljs-params\">string<\/span> }<\/span>) =&gt;<\/span> payload)\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">await<\/span> db.update(actionLog).set({ clientEnd: data.clientEnd }).where(eq(actionLog.id, data.id));\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\" id=\"the-server-1\">The Server<\/h3>\n\n\n\n<p>Let&#8217;s look at our server handler.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">    .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> traceId = crypto.randomUUID();\n\n      <span class=\"hljs-keyword\">const<\/span> start = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n        sendContext: {\n          loggingId: <span class=\"hljs-string\">\"\"<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>\n        }\n      });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We start by creating a <code>traceId<\/code>. This is the single identifier that represents the entirety of the action the user is performing; it\u2019s not a log id. In fact, for real observability systems, there will be many, many log entries against a single <code>traceId<\/code>, representing all the sub-steps involved in that action.<\/p>\n\n\n\n<p>For now, there&#8217;ll just be a single log entry, but in a bit we&#8217;ll have some fun and go a little further.<\/p>\n\n\n\n<p>Once we have the <code>traceId<\/code>, we note the start time, and then we call <code>await next<\/code> to finish our work on the server. We add a <code>loggingId<\/code> to the context we&#8217;ll be sending <em>back down<\/em> to the client. It&#8217;ll use this to update the log entry with the <code>clientEnd<\/code> time, so we can see the total end-to-end network time.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> end = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n\n<span class=\"hljs-keyword\">const<\/span> id = <span class=\"hljs-keyword\">await<\/span> addLog({\n  data: { actionName: name, clientStart: context.clientStart, traceId: traceId, duration: end - start },\n});\nresult.sendContext.loggingId = id;\n\n<span class=\"hljs-keyword\">return<\/span> result;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next we get the end time after the work has completed. We add a log entry, and then we update the context we&#8217;re sending back down to the client (the <code>sendContext<\/code> object) with the correct <code>loggingId<\/code>. Recall that the client callback used this to add the <code>clientEnd<\/code> time.<\/p>\n\n\n\n<p>And then we return the result, which then finishes the processing on the server, and allows control to return to the client.<\/p>\n\n\n\n<p>The <code>addLog<\/code> function is pretty boring; it just inserts a row in our log table with Drizzle.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> addLog = createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> })\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">payload<\/span>: <span class=\"hljs-params\">AddLogPayload<\/span><\/span>) =&gt;<\/span> payload)\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> { actionName, clientStart, traceId, duration } = data;\n\n    <span class=\"hljs-keyword\">const<\/span> id = crypto.randomUUID();\n    <span class=\"hljs-keyword\">await<\/span> db.insert(actionLog).values({\n      id,\n      traceId,\n      clientStart,\n      clientEnd: <span class=\"hljs-string\">\"\"<\/span>,\n      actionName,\n      actionDuration: duration,\n    });\n\n    <span class=\"hljs-keyword\">return<\/span> id <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The value of <code>clientEnd<\/code> is empty, initially, since the client callback will fill that in.<\/p>\n\n\n\n<p>Let&#8217;s run our Middleware. We&#8217;ll add it to a serverFn that updates an epic.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> updateEpic = \n  createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> })\n    .middleware(&#91;loggingMiddleware(<span class=\"hljs-string\">\"update epic\"<\/span>)])\n    .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">obj<\/span>: { <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">number<\/span>; <span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span> }<\/span>) =&gt;<\/span> obj)\n    .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; { <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-built_in\">Math<\/span>.random()));\n\n  <span class=\"hljs-keyword\">await<\/span> db.update(epicsTable)\n    .set({ name: data.name })\n    .where(eq(epicsTable.id, data.id));\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And when this executes, we can see our logs!<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"97\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=1024%2C97&#038;ssl=1\" alt=\"A database logging table displaying columns for id, trace_id, client_start, client_end, action_name, and action_duration, with several entries showing recorded data.\" class=\"wp-image-7462\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=1024%2C97&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=300%2C28&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=768%2C73&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=1536%2C145&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img1-1.png?resize=2048%2C194&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-problem\">The Problem<\/h2>\n\n\n\n<p>There&#8217;s one small problem: we have a TypeScript error.<\/p>\n\n\n\n<p>Here&#8217;s the entire middleware, with the TypeScript error pasted as a comment above the offending line<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">import<\/span> { createMiddleware } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">import<\/span> { addLog, setClientEnd } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/logging\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> loggingMiddleware = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span>\n<\/span><\/span><span class='shcb-loc'><span>  createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n<\/span><\/span><span class='shcb-loc'><span>    .client(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"middleware for\"<\/span>, name, <span class=\"hljs-string\">\"client\"<\/span>, context);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> clientStart = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n<\/span><\/span><span class='shcb-loc'><span>        sendContext: {\n<\/span><\/span><span class='shcb-loc'><span>          clientStart,\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>      });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> clientEnd = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n<\/span><\/span><mark class='shcb-loc'><span>      <span class=\"hljs-comment\">\/\/ ERROR: 'result.context' is possibly 'undefined'<\/span>\n<\/span><\/mark><mark class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> loggingId = result.context.loggingId;\n<\/span><\/mark><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">await<\/span> setClientEnd({ data: { id: loggingId, clientEnd } });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">return<\/span> result;\n<\/span><\/span><span class='shcb-loc'><span>    })\n<\/span><\/span><span class='shcb-loc'><span>    .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> traceId = crypto.randomUUID();\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> start = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n<\/span><\/span><span class='shcb-loc'><span>        sendContext: {\n<\/span><\/span><span class='shcb-loc'><span>          loggingId: <span class=\"hljs-string\">\"\"<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>      });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> end = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> id = <span class=\"hljs-keyword\">await<\/span> addLog({\n<\/span><\/span><span class='shcb-loc'><span>        data: { actionName: name, clientStart: context.clientStart, traceId: traceId, duration: end - start },\n<\/span><\/span><span class='shcb-loc'><span>      });\n<\/span><\/span><span class='shcb-loc'><span>      result.sendContext.loggingId = id;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">return<\/span> result;\n<\/span><\/span><span class='shcb-loc'><span>    });\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Why does TypeScript dislike this line?<\/p>\n\n\n\n<p>We call it on the client, after we call <code>await next<\/code>. Our server does in fact add a <code>loggingId<\/code> to its <code>sendContext<\/code> object. And it&#8217;s there: the value is logged.<\/p>\n\n\n\n<p>The problem is a technical one. Our server callback can see the things the client callback added to <code>sendContext<\/code>. But the client callback is <em>not<\/em> able to &#8220;look ahead&#8221; and see what the server callback added to <em>its<\/em> <code>sendContext<\/code> object. The solution is to split the Middleware up.<\/p>\n\n\n\n<p>Here&#8217;s a version 2 of the same Middleware. I&#8217;ve added it to a new loggingMiddlewareV2.ts module.<\/p>\n\n\n\n<p>I&#8217;ll post the entirety of it below, but it&#8217;s the same code as before, except all the stuff in the <code>.client<\/code> handler <em>after<\/em> the call to <code>await next<\/code> has been moved to a second Middleware. This new, second Middleware, which only contains the second half of the <code>.client<\/code> callback, then takes the other Middleware as its&nbsp;<em>own<\/em>&nbsp;Middleware input.<\/p>\n\n\n\n<p>Here&#8217;s the code:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { createMiddleware } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-start\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { addLog, setClientEnd } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\".\/logging\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> loggingMiddlewarePre = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span>\n  createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n    .client(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n      <span class=\"hljs-built_in\">console<\/span>.log(<span class=\"hljs-string\">\"middleware for\"<\/span>, name, <span class=\"hljs-string\">\"client\"<\/span>, context);\n\n      <span class=\"hljs-keyword\">const<\/span> clientStart = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n        sendContext: {\n          clientStart,\n        },\n      });\n\n      <span class=\"hljs-keyword\">return<\/span> result;\n    })\n    .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> traceId = crypto.randomUUID();\n\n      <span class=\"hljs-keyword\">const<\/span> start = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next({\n        sendContext: {\n          loggingId: <span class=\"hljs-string\">\"\"<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>,\n        },\n      });\n\n      <span class=\"hljs-keyword\">const<\/span> end = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n\n      <span class=\"hljs-keyword\">const<\/span> id = <span class=\"hljs-keyword\">await<\/span> addLog({\n        data: { actionName: name, clientStart: context.clientStart, traceId: traceId, duration: end - start },\n      });\n      result.sendContext.loggingId = id;\n\n      <span class=\"hljs-keyword\">return<\/span> result;\n    });\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> loggingMiddleware = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span><\/span>) =&gt;<\/span>\n  createMiddleware({ <span class=\"hljs-keyword\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span> })\n    .middleware(&#91;loggingMiddlewarePre(name)])\n    .client(<span class=\"hljs-keyword\">async<\/span> ({ next }) =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> next();\n\n      <span class=\"hljs-keyword\">const<\/span> clientEnd = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>().toISOString();\n      <span class=\"hljs-keyword\">const<\/span> loggingId = result.context.loggingId;\n\n      <span class=\"hljs-keyword\">await<\/span> setClientEnd({ data: { id: loggingId, clientEnd } });\n\n      <span class=\"hljs-keyword\">return<\/span> result;\n    });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>We export that <em>second<\/em> Middleware. It takes the other one as <em>its own<\/em> middleware. That runs everything, as before. But now when the <code>.client<\/code> callback calls <code>await next<\/code>, it knows what&#8217;s in the resulting context object. It knows this because that other Middleware is now <em>input<\/em> to <em>this<\/em> Middleware, and the typings can readily be seen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"going-deeper\">Going Deeper<\/h2>\n\n\n\n<p>We could end the post here. I don&#8217;t have anything new to show with respect to TanStack Start. But let&#8217;s make our observability system just a <em>little<\/em> bit more realistic, and in the process see a cool Node feature that&#8217;s not talked about enough, and also has the distinction of being the worst named API in software engineering history: <code>asyncLocalStorage<\/code>.<\/p>\n\n\n\n<p>You&#8217;d be forgiven for thinking <code>asyncLocalStorage<\/code> was some kind of async version of your browser&#8217;s <code>localStorage<\/code>. But no: it&#8217;s a way to set and maintain context for the entirety of an async operation in Node.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"when-server-functions-call-server-functions\">When Server Functions Call Server Functions<\/h3>\n\n\n\n<p>Let&#8217;s imagine our <code>updateEpic<\/code> server function also wants to <em>read<\/em> the epic it just updated. It does this by calling the <code>getEpic<\/code> serverFn. So far so good, but if our <code>getEpic<\/code> serverFn also has logging Middleware configured, we really would want it to use the <code>traceId<\/code> we already created, rather than create its own.<\/p>\n\n\n\n<p>Think about React context: it allows you to put arbitrary state onto an object that can be read by any component in the tree. Well, Node&#8217;s <code>asyncLocalStorage<\/code> allows this same kind of thing, except instead of being read anywhere inside of a component tree, the state we set can be read anywhere within the current async operation. This is exactly what we need.<\/p>\n\n\n\n<p>Note that TanStack Start did have a <code>getContext<\/code> \/ <code>setContext<\/code> set of api&#8217;s in an earlier beta version, which maintained state for the current, entire <em>request<\/em>, but they were removed. If they wind up being re-added at some point (possibly with a different name) you can just use them.<\/p>\n\n\n\n<p>Let\u2019s start by importing <code>AsyncLocalStorage<\/code>, and creating an instance.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> { AsyncLocalStorage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"node:async_hooks\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> asyncLocalStorage = <span class=\"hljs-keyword\">new<\/span> AsyncLocalStorage();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now let&#8217;s create a function for <em>reading<\/em> the traceId that some middleware <em>higher up<\/em> in our callstack <em>might<\/em> have added<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getExistingTraceId<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> store = asyncLocalStorage.getStore() <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">any<\/span>;\n  <span class=\"hljs-keyword\">return<\/span> store?.traceId;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>All that&#8217;s left is to <em>read<\/em> the <code>traceId<\/code> that was <em>possibly<\/em> set already, and if none was set, create one. And then, crucially, use <code>asyncLocalStorage<\/code> to <em>set<\/em> our <code>traceId<\/code> for any other Middleware that will be called during our operation.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">    .server(<span class=\"hljs-keyword\">async<\/span> ({ next, context }) =&gt; {\n      <span class=\"hljs-keyword\">const<\/span> priorTraceId = getExistingTraceId();\n      <span class=\"hljs-keyword\">const<\/span> traceId = priorTraceId ?? crypto.randomUUID();\n\n      <span class=\"hljs-keyword\">const<\/span> start = +<span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Date<\/span>();\n\n      <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> asyncLocalStorage.run({ traceId }, <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">await<\/span> next({\n          sendContext: {\n            loggingId: <span class=\"hljs-string\">\"\"<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>\n          }\n        });\n      });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The magic line is this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> asyncLocalStorage.run({ traceId }, <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">await<\/span> next({\n    sendContext: {\n      loggingId: <span class=\"hljs-string\">\"\"<\/span> <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">string<\/span>,\n    },\n  });\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Our call to next is wrapped in <code>asyncLocalStorage.run<\/code>, which means\u00a0<em>virtually\u00a0anything<\/em>\u00a0that gets called in there can see the\u00a0<code>traceId<\/code> we set. There are a few exceptions at the margins, for things like WorkerThreads. But any normal async operations which happen inside of the run callback will see the <code>traceId<\/code> we set.<\/p>\n\n\n\n<p>The rest of the Middleware is the same, and I&#8217;ve saved it in a loggingMiddlewareV3 module. Let&#8217;s take it for a spin. First, we&#8217;ll add it to our <code>getEpic<\/code> serverFn.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getEpic = createServerFn({ method: <span class=\"hljs-string\">\"GET\"<\/span> })\n  .middleware(&#91;loggingMiddlewareV3(<span class=\"hljs-string\">\"get epic\"<\/span>)])\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">string<\/span> | <span class=\"hljs-params\">number<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">Number<\/span>(id))\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">const<\/span> epic = <span class=\"hljs-keyword\">await<\/span> db.select().from(epicsTable).where(eq(epicsTable.id, data));\n    <span class=\"hljs-keyword\">return<\/span> epic&#91;<span class=\"hljs-number\">0<\/span>];\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now let\u2019s add it to\u00a0<code>updateEpic<\/code>,\u00a0and update it to\u00a0also call our <code>getEpic<\/code> server function.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> updateEpic = createServerFn({ method: <span class=\"hljs-string\">\"POST\"<\/span> })\n  .middleware(&#91;loggingMiddlewareV3(<span class=\"hljs-string\">\"update epic\"<\/span>)])\n  .inputValidator(<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">obj<\/span>: { <span class=\"hljs-params\">id<\/span>: <span class=\"hljs-params\">number<\/span>; <span class=\"hljs-params\">name<\/span>: <span class=\"hljs-params\">string<\/span> }<\/span>) =&gt;<\/span> obj)\n  .handler(<span class=\"hljs-keyword\">async<\/span> ({ data }) =&gt; {\n    <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\"><span class=\"hljs-params\">resolve<\/span> =&gt;<\/span> setTimeout(resolve, <span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-built_in\">Math<\/span>.random()));\n    <span class=\"hljs-keyword\">await<\/span> db.update(epicsTable).set({ name: data.name }).where(eq(epicsTable.id, data.id));\n\n    <span class=\"hljs-keyword\">const<\/span> updatedEpic = <span class=\"hljs-keyword\">await<\/span> getEpic({ data: data.id });\n    <span class=\"hljs-keyword\">return<\/span> updatedEpic;\n  });<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Our server function now updates our epic, and then\u00a0<em>calls<\/em>\u00a0the other serverFn to\u00a0<em>read<\/em>\u00a0the newly updated epic.<\/p>\n\n\n\n<p>Let&#8217;s clear our logging table, then give it a run. I&#8217;ll edit, and save an individual epic. Opening the log table now shows this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"98\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=1024%2C98&#038;ssl=1\" alt=\"A screenshot of a database table displaying log entries with columns for id, trace_id, client_start, client_end, action_name, and action_duration.\" class=\"wp-image-7463\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=1024%2C98&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=300%2C29&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=768%2C74&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=1536%2C147&amp;ssl=1 1536w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/img2-1.png?resize=2048%2C196&amp;ssl=1 2048w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Note there&#8217;s <em>three<\/em> log entries. In order to edit the epic, the UI first <em>reads<\/em> it. That&#8217;s the first entry. Then the update happens, and then the second read, from the <code>updateEpic<\/code> serverFn. Crucially, notice how the last two rows, the update and the last read, both share the same <code>traceId<\/code>!<\/p>\n\n\n\n<p>Our &#8220;observability&#8221; system is pretty basic right now. The\u00a0<code>clientStart<\/code>\u00a0and\u00a0<code>clientEnd<\/code>\u00a0probably don&#8217;t make much sense for these secondary actions that are all fired off from the server, since there\u2019s not really any end-to-end latency. A real observability system would likely have separate, isolated rows just for client-to-server latency measures. But combining everything together made it easier to put something simple together, and showing off TanStack Start Middleware was the goal, not creating a real observability system.<\/p>\n\n\n\n<p>Besides, we&#8217;ve now seen all the pieces you&#8217;d need if you wanted to actually build this into something more realistic: TanStack&#8217;s Middleware gives you everything you need to do anything you can imagine.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"parting-thoughts\">Parting Thoughts<\/h2>\n\n\n\n<p>We&#8217;ve barely scratched the surface of Middleware. Stay tuned for a future post where we&#8217;ll push middleware to its limit and achieve single-flight mutations.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>TanStack Start is one of the most exciting full-stack web development frameworks I&#8217;ve seen. I&#8217;ve written about it before. In essence, TanStack Start takes TanStack Router, a superb, strongly-typed client-side JavaScript framework, and adds server-side support. This serves two purposes: it gives you a place to execute server-side code, like database access; and it enables [&hellip;]<\/p>\n","protected":false},"author":21,"featured_media":7470,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[3,240],"class_list":["post-7452","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-javascript","tag-tanstack"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/10\/Introducing-TanStack-Start-Middleware.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7452","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7452"}],"version-history":[{"count":20,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7452\/revisions"}],"predecessor-version":[{"id":7513,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7452\/revisions\/7513"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7470"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7452"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7452"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7452"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}