Introduction
This is an opinionated link-first getting started guide. It is my preferred setup as of last authoring, and as such is also liable to change as I change tooling opinions over time, etc.
This guide will walk through setting up a Next.js app deployed on Vercel on a custom domain. The codebase will include/use Typescript, ESLint, Prettier, TailwindCSS, Jest, React Testing Library, Playwright, Github Actions, Github CLI, PWA support, and asdf.
Some aliases I will use on the command line are as follows: g
for git
, g ap
for git add -p
, g cm
for git commit -m
, and nr
for npm run
.
Prepare
asdf install nodejs [version]
asdf global nodejs [version]
brew update && brew upgrade gh
mkdir app
cd app
asdf local nodejs [version]
Setup Github Repository
g init
gh repo create app --public --source=.
echo "node_modules" >> .gitignore
g ap
g cm "First commit."
g push -u origin main
Next.js
npm i next react react-dom
// package.json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
echo ".next" >> .gitignore
Typescript
// pages/index.tsx
export default function Index() {
return (
<main>
<h1>App</h1>
</main>
);
}
Run nr dev
. Next.js will detect the .tsx
file and walk you through their preferred Typescript setup.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": false
}
}
Other settings than these will be generated by Next.js, these are just the two I want to mention. As of this writing my personal preference when coding TypeScript is to only use types when I want the type safety. My current approach to achieve this is keeping the compiler strict
while disabling noImplicitAny
, and writing every file (even ones without types) as .ts
or .tsx
.
ESLint
Recall that the following is already in your package.json
"scripts"
:
// package.json
{
"scripts": {
"lint": "next lint"
}
}
Run nr lint
to trigger the Next.js eslint setup.
TailwindCSS
- TailwindCSS
- TailwindCSS Doc: Next.js Setup Guide
- Next.js Official Example: With TailwindCSS
- PostCSS
- Next.js Doc: Customizing PostCSS Plugins
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
(As far as I can tell, following the TailwindCSS Next.js setup guide results in a css bundle with less PostCSS plugins than the default. It seems you have to manually reintroduce those plugins yourself, if you'd like, which I won't cover in this elaboration, but the links should lead you down the right path.)
/* lib/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
// tailwind.config.js
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./lib/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
// pages/_app.tsx
import "../lib/styles.css";
function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default App;
// pages/index.tsx
<main className="flex flex-col items-center">
<h1 className="text-6xl">App</h1>
</main>
Prettier
- Prettier
- TailwindCSS Blog on automatic class sorting with Prettier Plugin
- Prettier Doc: Configuration Setup
npm i -D prettier prettier-plugin-tailwindcss
// .prettierrc
{
"singleQuote": true,
"semi": false
}
Deploy to Vercel
There is a Vercel CLI, but since the developer experience of using the Vercel Dashboard GUI is nice. I prefer manually clicking through the deploy steps in the GUI.
Confirm everything is working locally with nr dev
. Then, commit your latest code and use the Vercel dashboard to deploy your app.
Custom Domain
- Google Domains
- Vercel Domains
- Vercel Doc: Adding apex and www domains
- Vercel Doc: Introduction to Domains and Vercel
- My note on domains: Subdomains, Apex Domains, and Canonical URLs
Once you've procured a domain and also deployed to Vercel, you can go to your Vercel project and navigate to Settings > Domains
to setup your domain. As of last authoring, I setup the following custom records in my domain settings.
Vercel project settings show you how to add a CNAME in your DNS resources. It will look something like:
[example.com] | A | 1-minute | 76.76.21.21
[www.example.com] | CNAME | 1-minute | cname.vercel-dns.com
Vercel takes care of SSL and apex domain to subdomain redirects behind the scenes.
Playwright
npm init playwright@latest
{
"scripts": {
"test": "playwright test"
}
}
I left most of the default settings intact. I enabled the two mobile-browser projects. Also, here are the settings I changed.
const config: PlaywrightTestConfig = {
reporter: process.env.CI ? "dot" : "list",
use: {
baseURL: "http://localhost:3000",
},
webServer: {
command: "npm run build && npm run start",
port: 3000,
reuseExistingServer: !process.env.CI,
},
};
Note that a local server is usually up via nr dev
, which might deviate from nr build && nr start
, but I intentionally do not use command: process.env.CI ? npm run build && npm run start : npm run dev
because, if the dev server is not running, I'd rather test against the most production-like build. That said, if the tests pass on local against nr dev
I'm confident enough to push to remote.
// tests/home.spec.ts
import { test, expect } from "@playwright/test";
test("home page has header", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading")).toContainText("App");
});
CICD with Github Actions
npm init playwright@latest
sets up a good, minimal cicd yaml file at .github/workflows/playwright.yml
. The only change I make is getting rid of a reference to a master
branch to avoid confusion (I only use main
and dev
).
Embrace a PR Workflow
A note about workflow: Vercel branding is "Deploy. Preview. Ship.", where "Preview" is for checks, quality assurance, etc., so a trunk-based workflow where tests run before deploy is not the Vercel way. Instead, a branch-based paradigm is preferred, where tests are ran on PRs while simultaneously being deployed to a preview URL. Vercel deploy hooks will let you work around this, but it gets non-trivial. Just keep in mind: If you push directly to main it will immediately try to deploy, no matter your pipeline.
My current strategy is to lean in. I want as little maintenance as possible (no code is the best code). So, I have a dev
branch I work on and will submit PRs to the main
branch. This will trigger the CICD pipeline and failing tests will prevent merging to main.
Skooh
npm i -D skooh
// package.json
{
"scripts": {
"prepare": "skooh"
},
"hooks": {
"pre-commit": "npm run lint && npm run format
}
}
(Because it feels a bit too self-aggrandizing to recommend my own tool, I will mention here that husky is a great alternative git hooks manager.)
Since Vercel is "deploy first, preview second", you might consider heavy hook usage to prevent erroneous deployments to main/prod.
PWA
// public/manifest.json
{
"name": "App",
"short_name": "App",
"icons": [...],
...
}
// pages/_app.tsx
export default function App() {
return (
<>
<Head>
<link rel="manifest" href="/manifest.json" />
</Head>
</>
);
}
Other resources for PWA:
Next Steps
- Frontend Resources
- Nextjs
- Nextjs Markdown Blog Setup
Conclusion
Thanks for reading :D