Key Concepts

Project structure

For now, nostalgie doesn't provide any tooling to help scaffold out a project and makes some assumptions about how your project should be structured. Later, we plan to add the ability to allow projects to configure the structure.

Application entrypoint

Default: ./src/App.[jsx|tsx]

Note: This file must exist.

Nostalgie expects to find a ./src/App.[jsx|tsx] file that it will treat as the only entrypoint into your application. It uses react-router behind the scenes to unify front- and back-end routing. Nostalgie is NOT organized on the basis of filesystem routing. Instead, it is expected that your application entrypoint makes all routing decisions.

For example, if you want / (home) and /docs (docs) routes, you might have an ./src/App.tsx file like the following. In this example, we've effectively declared two routes and have code-splitting supported between them but we're not married to that sort of thinking.

import { Switch, Route } from 'nostalgie';
import * as React from 'react';

const LazyDocsPage = React.lazy(() => import('./pages/Docs'));
const LazyHomePage = React.lazy(() => import('./pages/Home'));

export default function App() {
  return (
    <Switch>
      <Route exact path="/">
        <LazyHomePage />
      </Route>
      <Route path="/docs">
        <LazyDocsPage />
      </Route>
    </Switch>
  );
}

Functions entrypoint

Default: ./src/functions.[js|ts]

Note: When this file does not exist, function support is disabled.

In Nostalgie, invoking server-side code has never been easier. Nostalgie abstracts away the complexity of invoking server-side logic from the frontend in a way that these functions can be called either at runtime in the browser, or at render-time on the server.

Creating an exposing a function to your application code is as simple as defining and exporting a function. All server functions must be exported by the functions entrypoint but their actual logic can be imported from other files. You can structure this as you choose.

Authentication is not yet implemented but the ctx argument is already wired up and this will be the place where identity metadata will be made available.

import type { ServerFunctionContext } from 'nostalgie';

const posts = {
  {
    id: 1,
    title: 'Introducing Nostalgie',
    body: '...',
    author: 'ggoodman',
  },
  {
    id: 2,
    title: 'Introducing MDX support',
    body: '...',
    author: 'cooldood42',
  },
};

export function getBlogPosts(ctx: ServerFunctionContext, author?: string) {
  return author ? posts.filter(post => post.author === author) : posts;
}

If we want to use this function from our application code, we use the useQueryFunction hook exposed by nostalgie/functions. Note that we also import a reference to the actual function. This might sound surprising because that function often won't be able to execute in the browser context. In fact, we probably don't want it and its dependencies increasing the size of our front-end bundle(s). Have no fear, since Nostalgie is also responsible for packaging applications, it will actually replace the functions entrypoint (and any transient dependencies) with a simple object providing the metadata about which functions exist. Importing a reference to the actual function also has the side benefit of helping us get accurate type hints. When using useQueryFunction below, we will be hinted with the names and types of the arguments array. We will also have full type hinting for the blogPosts value.

Behind the scenes, the useQueryFunction hook is delegating to useQuery from react-query. As a result, the returned blogPosts object is an observer of the query. In fact, we could have provided a third argument to useQueryFunction to set caching and other options as supported by useQuery.

import * as React from 'react';
import { useQueryFunction } from 'nostalgie/functions';
import BlogPost from './BlogPost';
import { getBlogPosts } from './functions';

export default function BlogPostList() {
  const blogPosts = useQueryFunction(getBlogPosts, ['ggoodman']);

  if (blogPosts.isLoading) return 'Loading...';

  if (blogPosts.error) return 'An error has occurred: ' + blogPosts.error.message;

  return (
    <div>
      {blogPosts.data.map((post) => (
        <BlogPost key={post.id} post={post} />
      ))}
    </div>
  );
}

Development workflow

Once you've scaffolded your Nostalgie project, you're ready to start developing it. The nostalgie cli supports a dev command to make this easy. It will build your app and run it. Anytime you make changes to your app, it will stop the server, rebuild your app and restart the server. Because we're using esbuild, compilation should be an order of magnitude faster than tools you might be used to using.

nostalgie dev --port 9090 --env development --root-dir ./example

To see other available options:

nostalgie dev --help

Production workflow

Now that you've built and played with your app locally, it's time to deploy. Nostalgie will eventually support building for many different targets. For the time being, it only supports a node or dockerized node environment.

nostalgie build --root-dir ./example

The above will produce artifacts in the ./example/build dirctory, suitable for running the application locally via node ./example/build or via docker using the ./example/build/Dockerfile. Nostalgie build artifacts have no further build steps required and do not require any node_modules at runtime.