Create a TypeScript Project and Development Workflow
A TypeScript Next.js project is more than a folder created by a scaffold command. It is a working agreement between the App Router, React, the TypeScript compiler, your package manager, and the development server. The outcome of this lesson is a clean project that starts reliably, reports type mistakes before they ship, and gives you a repeatable workflow for editing pages, components, configuration, and dependencies.
In this course, the later App Router lessons assume you can read the project tree and predict which files affect routing, rendering, styling, linting, and builds. This chapter gives you that baseline. You will create a small TypeScript project, inspect the generated files, make progressive changes, diagnose common startup and type errors, and leave with a workflow you can reuse for future full-stack Next.js work.
What the Scaffold Creates
The usual starting point is create-next-app. It asks a small set of questions, installs dependencies, and writes a project layout that Next.js understands. The command does not create a special binary project format. It creates ordinary files: a package.json with scripts, an app directory for App Router routes, a TypeScript configuration, a Next.js configuration file, and styling entry points. Next.js then interprets these files at development and build time.
In an App Router project, directories inside app are route segments. A page.tsx file makes a URL reachable. A layout.tsx wraps that route segment and its children. A loading.tsx can provide a loading boundary, and error.tsx can provide an error boundary for client-side recovery. These names are conventions, not imports you register manually. The router scans the file tree and builds a route graph from those conventions.
TypeScript sits beside that router. Files ending in .ts and .tsx are checked according to tsconfig.json. Next.js also writes a generated type folder during development and build so route-related types can participate in checking. The important trade-off is that TypeScript checks your program before runtime, but it cannot prove that every network response, environment variable, or user input is valid. You still validate data at runtime when it crosses into your app.
Project Anatomy
| File or directory | Role in the workflow |
|---|---|
app/layout.tsx |
Root HTML shell shared by all App Router pages. |
app/page.tsx |
Home route for /; editing it should hot refresh in development. |
package.json |
Declares dependencies and scripts such as dev, build, start, and optional lint checks. |
tsconfig.json |
Controls TypeScript checking, module resolution, JSX handling, and path aliases. |
next.config.ts |
Holds framework configuration for Next.js behavior. |
The development server watches this tree. When you edit a component, React Fast Refresh tries to preserve local state while updating the browser. When you edit route files or configuration, Next.js may rebuild a larger part of the graph or require a restart. Understanding this difference makes troubleshooting faster: component changes are usually hot, configuration and dependency changes often need a process restart.
Command and Configuration Anatomy
A minimal TypeScript App Router setup can be created with explicit flags so the result is predictable in a team setting. The exact package manager is a team choice; use the one your repository already uses when joining an existing codebase.
npx create-next-app@latest course-next --ts --app --eslint --src-dir --import-alias "@/*"
cd course-next
npm run dev
The deterministic part is the workflow: a directory named course-next is created, dependencies are installed, and npm run dev starts a local server, commonly on http://localhost:3000 unless that port is busy. The flags request TypeScript, the App Router, linting support, a src directory, and the alias @/* for imports from src.
The generated scripts are the commands you will use most often. Their names are stable even when the underlying Next.js internals evolve.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
dev runs the watcher and local compiler. build performs the production compilation and catches many route, type, and rendering problems that a casual browser check can miss. start serves the production build; it is not a substitute for dev. If the project includes a lint script, run it before build so style and likely mistakes are caught earlier.
Example 1: A Typed Page
Start with the home page. In a src-based App Router project, src/app/page.tsx exports a React component for the / route. The component below uses a typed array so the editor and compiler know which fields each lesson card has.
type LessonCard = {
title: string;
minutes: number;
};
const lessons: LessonCard[] = [
{ title: "Project setup", minutes: 20 },
{ title: "App Router files", minutes: 25 }
];
export default function HomePage() {
return (
<main>
<h1>Next.js TypeScript workflow</h1>
<ul>
{lessons.map((lesson) => (
<li key={lesson.title}>
{lesson.title}: {lesson.minutes} minutes
</li>
))}
</ul>
</main>
);
}
Expected behavior: visiting / renders the heading and two list items. If you accidentally write {lesson.minute}, TypeScript reports that the property does not exist. That error is useful because the route would otherwise render incomplete information or fail later after more code depends on the wrong field name.
Example 2: A Typed Component Boundary
Move repeated UI into a component when it has a clear input shape. This keeps the page focused on route composition while the component owns the display contract.
type WorkflowStepProps = {
label: string;
done: boolean;
};
export function WorkflowStep({ label, done }: WorkflowStepProps) {
return (
<li aria-current={done ? undefined : "step"}>
<span>{done ? "Done" : "Next"}</span> {label}
</li>
);
}
Expected behavior: <WorkflowStep label="Run the dev server" done={false} /> renders a list item marked as the current step. Passing done="false" is rejected because the component expects a boolean, not a string. This is a small example of a broader workflow rule: make invalid component usage hard to write.
Example 3: Environment Variables With Runtime Checks
TypeScript cannot know whether a deployment environment has the variables your app expects. Keep server-only values unprefixed, expose browser values only with NEXT_PUBLIC_, and check required values at startup or request time.
export function getRequiredEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const apiBaseUrl = getRequiredEnv("COURSE_API_BASE_URL");
console.log(apiBaseUrl);
Expected behavior: when COURSE_API_BASE_URL is defined, the function returns it as a string. When it is missing or empty, the process throws a clear error naming the missing variable. This is better than allowing undefined to become part of a fetch URL and debugging a vague network failure later.
Design Choices and Trade-offs
The scaffold questions are not cosmetic. TypeScript adds compile-time feedback but requires you to model props, data, and utilities deliberately. The App Router gives you route layouts, Server Components, and nested loading states, but it asks you to learn file conventions instead of central route registration. A src directory separates application source from repository-level configuration, which is helpful as the project grows. A path alias such as @/* reduces fragile relative imports, but it should match the actual directory layout and be understood by test tools if you add them later.
Strict TypeScript settings catch more mistakes earlier, especially nullable data and accidental any. The cost is more explicit code at boundaries. Keep strictness high for new projects unless you are migrating a large JavaScript codebase. In migration work, raise strictness incrementally so the team can keep shipping while replacing uncertain types with real domain models.
Failure Modes and Troubleshooting
Symptom: npm run dev starts on a different port or refuses to start. Cause: another process is using the default port, or dependencies did not install cleanly. Diagnose: read the terminal output, check whether the reported local URL changed, and run npm install if modules are missing. Correction: open the URL the server prints, stop the conflicting process, or start with an explicit port such as npm run dev -- -p 3001.
Symptom: imports using @/ fail even though relative imports work. Cause: the alias in tsconfig.json does not match the directory layout, or the file was moved outside the configured base path. Diagnose: inspect compilerOptions.paths and confirm whether the target files live under src. Correction: align the alias with the chosen project structure and restart the dev server so tooling reloads the configuration.
Symptom: the browser updates for component edits but ignores changes to next.config.ts or environment variables. Cause: some configuration is read when the dev process starts. Diagnose: compare hot component edits with process-level changes. Correction: stop and restart npm run dev after configuration or .env.local edits.
Symptom: npm run build fails even though the page looked fine in development. Cause: the production build performs stricter route analysis, type checking, and optimization. Diagnose: run the build locally before opening a pull request and read the first compiler error, not the last cascade. Correction: fix the typed source problem, remove invalid imports across server and client boundaries, or add the runtime data checks the route requires.
Security, Performance, and Reliability
A clean workflow reduces operational risk. Keep secrets in .env.local for local development and out of source control. Only variables prefixed with NEXT_PUBLIC_ are intended for browser exposure, so never put private tokens behind that prefix. Run npm run build because it exercises production compilation and can reveal problems hidden by the forgiving feedback loop of development mode.
Performance work starts early with dependency choices. Every client component and browser dependency can increase client JavaScript. Keep components server-rendered by default in the App Router and add "use client" only where interactivity requires it. Reliability comes from making the common commands boring: install, dev, lint, build, and start should mean the same thing on every teammate’s machine and in continuous integration.
Hands-on Lab
Prerequisites: install a current Node.js runtime supported by your chosen Next.js version, have a terminal, and choose one package manager for the project. The steps below use npm.
- Create the app with
npx create-next-app@latest course-next --ts --app --eslint --src-dir --import-alias "@/*". - Enter the directory with
cd course-nextand start the server withnpm run dev. - Open the printed local URL and confirm that the starter page renders.
- Replace
src/app/page.tsxwith the typed page from Example 1. - Create a component file such as
src/components/workflow-step.tsxand add the component from Example 2. - Import the component into the page and render one completed step and one current step.
- Add
COURSE_API_BASE_URL=http://localhost:4000to.env.local, then restart the dev server before using the helper from Example 3. - Run
npm run buildand fix any reported type or route errors.
Verification: the home route renders your typed lesson list, the workflow component rejects incorrect prop types in the editor or build, and npm run build completes. Cleanup: stop the dev server with Ctrl+C. If this was only a practice project, remove the course-next directory; otherwise commit the scaffold, page, component, and configuration together so the baseline is reproducible.
Assessment Exercises
- A teammate adds
done="true"toWorkflowStep. Explain why TypeScript rejects it and what bug that prevents. - Your project uses
@/components/Button, but the build cannot resolve the import. Which configuration file do you inspect first, and what mapping should you expect in asrc-based project? - Why can an environment variable bug survive TypeScript checking, and where would you add a runtime guard?
- Describe when you would restart the development server instead of expecting Fast Refresh to apply a change.
- Compare strict TypeScript for a new project with gradual strictness for a migration. What does each choice optimize for?
Summary
A TypeScript Next.js workflow works because ordinary files are connected by framework conventions and compiler rules. The App Router maps named files to routes and layouts, TypeScript checks the shapes you declare, scripts provide repeatable commands, and the dev server turns edits into fast feedback. Treat the scaffold as the first design decision in the project, then verify the workflow with real edits, clear type failures, environment checks, and a production build.
