Hunter Henrichsen

Hunter Henrichsen

Search Circle
< lecture

Lecture 05 - Q&A Session 1

posted over 1 year ago 13 min read

Lecture 05 - Q&A Session 1#

What’s the best way to go about server side rendering in NextJS? Should we be making our initial API calls there to optimize SEO?#

Next.js gives a couple of rendering modes for us to use:

Static#

Static rendering is default for the app router, or done via getStaticProps in the pages router. Pages are generated at build time, or in the background when data is revalidated on the app router (or using ISR on the pages router).

Serverside#

Used whenever dynamic data is loaded in the app router, or via useServerSideProps in the pages router.

Streaming / Suspense (on the app router)#

Streaming allows portions of the app that can be to be rendered statically, and then the rest of them to be rendered on the server of the client. You can use a <Suspense> component to define the boundaries of where that should be, and a loading.ts file to determine how that should work.

Clientside#

Ideally, pages where SEO and indexability matter (like landing pages, or other public pages) should be static where possible, or server-rendered if not.

How do I do authentication, and how does it work?#

Here’s an example of what this looks like (very loosely) for OAuth. You should use OAuth. People trust it more, it’s more straightforward, they don’t have to remember passwords, and you don’t have to worry about accidentally leaking that.

Once you’re logged in, you can use Bearer or Session based authentication. I think bearer is neat because you can keep more data in there without needing to query the database, but sessions have worked for a long while and are very standard.

This diagram can also be accessed here.

I recommend using NextAuth (as long as you don’t use a Credentials type), Clerk, Auth0, or Supabase Auth. All of these have plenty of guides and integrate well with Next.js and other frameworks.

There is a lot of nuance within Authentication–do we want to move that lecture up so that it’s next?

What are the best resources I can use to learn how to think in systems and get good at architecting them?#

For a Specific Case#

One way I interpret this question is “How do I build a good architecture for the problem I’m trying to solve?” Here’s how I think about that:

Define Your Requirements#

Figure out what needs to happen in order for you to build what you’re trying to build. Break it down into pieces with enough detail that they can be accomplished individually. Keep track of new requirements and flesh them out as needed.

Implement The Requirements#

Implement the pieces that have been created and fleshed out.

Analyze#

Use data and feedback to determine what is working and what is not working. Things that are not working become new requirements. If some of the parts were implemented suboptimally, file them as tech debt and requirements for the next time around.

Iterate#

Start over again with any new requirements.

I find that there are few decisions you can make that permanently lock you into a bad solution. Even when choosing tech stack, there are ways to slowly convert to another one, or migrate from a data source. The easiest way to learn what doesn’t work is to build something that doesn’t work, and use that experience to build towards something that does.

Other Ways to Learn#

Look at Prior Art#

Prior art is a good way to learn without making mistakes yourself. Here are some of the things I have looked at for system design:

There are many more

Ask Questions#

More experienced people than I talk frequently about things they’re working on and designs they’ve done in the past. You can consume their content (many have blogs, twitter accounts, etc.), but also many of them are fairly active in a way that lets you ask your own questions.

The #engineering channel is also an awesome resource here if you need advice or a reviewer, which brings me to the next way: having someone else review what you’ve made.

Review Them#

Have someone poke holes in the architecture you’re designing. You know the desired, happy path as the one who designed it, but another person can help to find things that you haven’t yet considered.

What other backend programming languages are good?#

Here is a list of them that I have used and liked:

Ultimately, I also kind of read this as “What backend programming language should I use?” which I would respond to with “use what you can develop quickly with”. It’s up to you to decide if learning something new is useful.

How should we set up a VPN for a staging environment? Or is that important at our stage of things?#

You can get a $6/month server at Vultr or other similar providers and install OpenVPN on there.

How should we set up a testing framework for frontend and backend? How early should we set this up? Prototype? Beta? MVP? Later?#

How early should we set this up?#

Set it up now. It’ll take 5 minutes to get set up and not much longer than that to write some initial tests, and then start enforcing them on PRs. I don’t think it’s worth it to shoot for 100% coverage initially, but you should have a framework in place and ensure that it runs on your core flows.

Setting up Vitest#

For individual components and unit tests, vitest is a recent favorite of mine. Here’s how I’d add that to a component:

cd apps/web
pnpm i -D vitest @vitejs/plugin-react @testing-library/jest-dom @testing-library/react jsdom
cd -

apps/web/vitest.config.ts

import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
},
});

apps/web/app/component.tsx

import { useState } from "react";
export default function Counter(): JSX.Element {
const [count, setCount] = useState(0);
return (
<>
<h2>{count}</h2>
<button
onClick={() => {
setCount(count + 1);
}}
type="button"
>
+
</button>
</>
);
}

apps/web/app/component.test.tsx

import { fireEvent, render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
import Component from "./component";
test("Heading should increment on button click", () => {
render(<Component />);
expect(screen.getByRole("heading", { level: 2, name: "0" })).toBeDefined();
fireEvent.click(screen.getByRole("button"));
expect(screen.getByRole("heading", { level: 2, name: "1" })).toBeDefined();
});

I can trigger this with pnpm vitest, but I’d rather add a script: apps/web/package.json

{
"scripts": {
"test": "vitest"
}
}

This way I can add more steps to testing, and use turbo to run tests.

Setting Up Playwright#

Playwright is used to write end to end tests across multiple platforms. Here’s how I’d add that to my project.

cd apps/web
pnpm i -D @playwright/test
cd -

We may also have to install the browsers that playwright uses:

pnpm exec playwright install

Now we create a config, setting some URLs so we use the local dev server: playwright.config.ts

import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "e2e",
reporter: "html",
use: {
baseURL: "http://127.0.0.1:3000",
screenshot: "only-on-failure",
},
// Run your local dev server before starting the tests
webServer: {
command: "pnpm run start",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
stdout: "ignore",
stderr: "pipe",
},
});

And write a test: apps/web/e2e/home.spec.ts

import { expect, test } from "@playwright/test";
test("should navigate the 404 page when clicking my invalid link", async ({
page,
}) => {
await page.goto("/");
// Save a screenshot; this will be generated the first time
await expect(page).toHaveScreenshot("home.png");
await page.click("text=Game List");
await expect(page).toHaveURL("/games");
// Save another screenshot
await expect(page.locator("h1")).toContainText("404");
await expect(page).toHaveScreenshot("404.png");
});

You can run this with pnpm playwright test, but I’m going to add a script for this as well:

apps/web/package.json

{
"scripts": {
"test:e2e": "playwright test"
}
}

This will fail the first time we run it (to generate the screenshots), but should pass as long as the page is deterministic from there.

If you need to regenerate screenshots, you can run pnpm test:e2e --update-snapshots.

You can also run this with pnpm test:e2e --ui to see the test as it works, and see what it sees at each step of the way.

Here are some other script changes that I made and feel obligated to show:

turbo.json

{
"pipeline": {
"test": {
"dependsOn": ["^db:generate"]
},
"test:e2e": {
"dependsOn": ["^db:generate"]
}
}
}

package.json

{
"scripts": {
"test:e2e": "turbo run test:e2e",
"test": "turbo run test"
}
}

There are also cool articles out there about getting this working with authentication and such, so I’d recommend checking those out too.

CI with GitHub Actions#

Not too much to say here; I tried to add a bunch to this file so you can see how it works. I try to do some caching here because there are some really slow steps that can take advantage of caching.

.github/workflows/tests.yml

name: Verify
on:
push:
branches: [main, next]
pull_request:
branches: [main]
jobs:
build:
name: Build and Run Tests
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.38.0-jammy
services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
--health-retries 5
env:
POSTGRES_PRISMA_URL: postgresql://postgres:postgres@postgres:5432/postgres
POSTGRES_URL_NON_POOLING: postgresql://postgres:postgres@postgres:5432/postgres
CI: true
NEXTAUTH_SECRET: supersecret-supersafe
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Check Format
run: pnpm format
- name: Build
run: pnpm build
- name: Run migrations
run: pnpm db:deploy
- name: Run unit tests
run: pnpm test
- name: Run e2e tests
run: pnpm test:e2e
- uses: actions/upload-artifact@v2
if: always()
with:
name: playwright-report
path: apps/web/playwright-report/
retention-days: 30

You may need to use Docker to update your screenshots if your CI runs on a different OS than your development environment. There’s also a discussion about testing a t3 repo here that may be of interest to you.

What is the best way to use tRPC in our Next.js Frontend App using Dependency Injection and services? Could we do a similar dependency injection on the backend?#

React Context vs DI#

There’s a neat article here about the difference, and how you can use react context to achieve something similar to DI without needing to add a whole DI framework. If that will work for you, it might not be worth the full investment in DI now. However, DI can be useful in testing and in codebase organization.

There’s also a good article here by Martin Fowler about why you might use Dependency Injection, specifically service locators like DI frameworks.

State Management#

This idea originally came up in a conversation about state management. In my opinion, context should not be used for state management, but can be used to facilitate state management by providing state management dependencies.

Setting Boundaries#

I’m going to add a package called server-only which will help me ensure that certain things (like files that use environment variables) remain in the server bundle and error my development environment if I try to add them to the client bundle.

cd apps/web
pnpm i -S server-only
cd -

Now, any package into which we import server-only will throw an error if we try to load it on the client, which makes it kind of the opposite of the 'use client' string. I’m going to use this to keep my injectors separate.

Adding a DI Framework#

I’m also going to add inversify and reflect-metadata here, as well as a couple dev dependencies that are required for the client side to work as expected. reflect-metadata allows us to save information about classes and types at runtime, allowing us to do things like ask for specific dependencies. This is required for anything like dependency injection. inversify is one of the available libraries that uses reflect-metadata to inject requirements into classes.

There are also a couple babel dependencies here so that we can add polyfills to the frontend for different features that aren’t standard yet, in this case decorators and typescript metadata for those.

cd apps/api
pnpm i -S inversify reflect-metadata
cd -

And let typescript know we want to support decorators as well, so we’ll add this to our tsconfig:

apps/web/tsconfig.json

{
"compilerOptions": {
"experimentalDecorators": true
}
}

And I’m going to import reflect-metadata in whatever the start location of my app is, in this case my _app.ts. On the app router, I can use the experimental instrumentation feature to do this instead.

Doing DI#

DI looks mostly the same wherever you do it, so I’m going to provide a better example here using a past project of mine; a discord bot.

Since bots are largely just responding to events of many types, it made sense to abstract some of this away so I could easily add responses to different emojis, for example.