Getting Started with NextJS on Vercel

Image of Author
February 24, 2022 (last updated October 5, 2022)


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.


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=.
g push -u origin main

g = git and (further down) nr = npm run

Create Next.js App

npm i next react react-dom
// package.json

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start"


// pages/index.tsx

export default function Index() {
  return (
      <HelloWorldComponent />

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

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.


// package.json
  "scripts": {
    "lint": "next lint"

Run nr lint to trigger the Next.js eslint setup.


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.)

/* src/styles.css */

@tailwind base;
@tailwind components;
@tailwind utilities;
// tailwind.config.js

module.exports = {
  content: ["./pages/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  plugins: [],
// pages/_app.tsx

import "../src/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>


npm i -D prettier prettier-plugin-tailwindcss
// package.json

  "prettier": {
    "singleQuote": true,
    "semi": false,
    "arrowParens": "avoid"

Deploy to Vercel

There is a Vercel CLI, but since the DevX for the Vercel Dashboard is nice, I prefer manually clicking through the deploy steps.

Attach Custom Domain

Vercel project settings show you how to add a CNAME in your DNS resources. It will look something like:

[custom domain name] | CNAME | 1-hour |


npm i -D @playwright/test
npx playwright install
  "scripts": {
    "test": "playwright test"
// playwright.config.ts

import { PlaywrightTestConfig } from "@playwright/test";

const config: PlaywrightTestConfig = {
  forbidOnly: !!process.env.CI, // fail if test.only makes it to CI
  retries: process.env.CI ? 2 : 0, // retries only on CI
  workers: process.env.CI ? 1 : undefined, // opt out of parallel tests on CI
  reporter: "dot",
  use: {
    baseURL: "http://localhost:3000",
  webServer: {
    command: "npm run build && npm run start",
    port: 3000,
    reuseExistingServer: !process.env.CI, // allow local dev server when running locally

export default config;

The above are some of the most important settings, in my opinion. webServer is how you will run the server on CI and baseURL is how you will use relative paths in your tests.

import { test, expect } from "@playwright/test";

test("greets user", async ({ page }) => {
  await page.goto("/");
  const main = page.locator("main");
  await expect(main).toHaveText(/App/);

CICD with Github Actions

# .github/workflows/ci.yml

name: CI
    branches: [main, dev]
    branches: [main]
    timeout-minutes: 60
    runs-on: ubuntu-latest
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
          node-version: "16.x"
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test
      - uses: actions/upload-artifact@v2
        if: always()
          name: playwright-report
          path: playwright-report/
          retention-days: 30

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 (aka, checks?) 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 and will PR to main as a way of "forcing" failed tests to prevent getting red code on prod.


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", so you might consider heavy hook usage to prevent erroneous deployments to main/prod.


The Next.js PWA example puts the manifest link in _app.tsx instead of _document.tsx. I'll admit to occasional confusion regarding what content goes in which file. In my opinion, a manifest link is best in _document.tsx so that's where I put it. But, probably trust whatever the PWA example says, I guess.

// public/manifest.json

  "name": "App",
  "short_name": "App",
  "icons": [...],
// pages/_document.tsx

export default function Document() {
  return (
        <link rel="manifest" href="/manifest.json" />

Other resources for PWA:

Add Domain Logic

For example, if I wanted to build an in-memory todo app, I would probably rip some of the code from the end of my blog post Reinventing Redux through React Refactors.


Thanks for reading :D