TypeScript with NextJS introduction

Starter configuration of a basic Next.js application with TypeScript. Updated for Next.js 16 (App Router).

Vladimír NeporAuthorVladimír Nepor
Published

Next.js is an open-source web development framework created by the private company Vercel providing React-based web applications with server-side rendering and static rendering.

Key Next.js Concepts

  • File-based routing: Every file in /app is a page.
  • SSR vs. SSG: Choose between Server-side Rendering and Static Site Generation.
  • API Routes: Define backend API inside /app/api.
  • Server vs. Client Components: Server components run on the backend, client components use "use client".
  • Proxy (formerly Middleware): Request handling before a route is rendered (redirects, rewrites, auth checks).

Creating Next.js app

  1. Installation

    From the terminal / command tool window:

    npx create-next-app@latest <appName>
    On choosing options selection:
    • Would you like to use the recommended Next.js defaults? choose No, customize settings
    • Would you like to use TypeScript? choose Yes
    • Which linter would you like to use?? choose ESLint
    • Would you like to use React Compiler? choose No
    • Would you like to ise Tailwind CSS? choose Yes
    • Would you like your code inside a `src/` directory? choose Yes
    • Would you like to use App Router? choose Yes
    • Would you like to customize the import alias (`@/*`)? choose No

    Launch the app

    cd <appName>
    npm run dev

    Visit: http://localhost:3000 to see the app.

  2. File-based Routing

    Each file inside /app defines a route automatically.

    • /app/page.tsx → / (Home page)
    • /app/about/page.tsx → /about

    Create a Basic Page

    Create /app/about/page.tsx:

    export default function AboutPage() {
    	return <h1>About Me</h1>;
    }

    Accessible at /about.

  3. Dynamic Routing

    For dynamic pages, create a folder with [param] notation.

    Create /app/blog/[id]/page.tsx:

    export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
    	const { id } = await params;
    	return <h1>Blog Post ID: {id}</h1>;
    }

    Accessible at /blog/1, /blog/2, etc.

    Since Next.js 15, params and searchParams are Promises and have to be awaited. The synchronous access shown in older tutorials (params.id) no longer works in Next.js 16.

  4. API Routes

    Use /app/api for backend routes.

    Create /app/api/hello/route.ts:

    export async function GET() {
    	return Response.json({ message: "Hello from Next.js API!" });
    }

    Accessible at /api/hello.

  5. Fetch Data on the Server (SSR)

    Server-side fetching is done inside page.tsx.

    export default async function Page() {
    	const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    	const post = await res.json();
    	return <h1>{post.title}</h1>;
    }

    Since Next.js 15, fetch requests are not cached by default. To cache the response, use e.g. fetch(url, { next: { revalidate: 3600 } }) or the "use cache" directive (Cache Components) in Next.js 16.

  6. Client vs. Server Components

    • By default, components in /app are Server Components.
    • To use state, effects, or event listeners, add "use client" at the top.

    Example:

    "use client";
    
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increase</button>
        </div>
      );
    }
  7. Proxy (formerly Middleware)

    Proxy allows you to handle requests before they reach the page. In Next.js 16, middleware.ts was renamed to proxy.ts (the exported function to proxy) and runs on the Node.js runtime. The old middleware.ts is deprecated.

    Create /proxy.ts (or /src/proxy.ts when using the src directory):

    import { NextResponse, type NextRequest } from 'next/server';
    
    export function proxy(req: NextRequest) {
      if (!req.cookies.get('auth')) {
        return NextResponse.redirect(new URL('/login', req.url));
      }
      return NextResponse.next();
    }
    
    export const config = {
      matcher: ['/dashboard/:path*'],
    };
  8. Deployment

    Build the production version locally:

    npm run build
    npm run start

    Deploy on Vercel with the Vercel CLI (or by connecting the Git repository in the Vercel dashboard):

    npm i -g vercel
    vercel --prod

    For deployment on Cloudflare Workers see Next.js on Cloudflare.

NextJS on Cloudflare Workers

Visit NextJS on Cloudflare Workers page.

Next.js vs. Express.js?

FeatureExpress.jsNext.js
RoutingManual route definitionsAutomatic based on file structure
RenderingManual template/data handlingSupports SSR, SSG, ISR
APICustom REST or GraphQLBuilt-in API routes under /app/api
OptimizationManual performance tweaksAutomatic optimizations

When to Use Next.js Over Express?

  • ✅ Need backend + frontend in one framework.
  • ✅ Better SEO with Server-side rendering.
  • ✅ Simple API routing without Express setup.
  • ✅ Automatic optimizations and better performance.

❌ If you're building a pure API backend, Express is still a better choice.

Optimized React Workflow for updating Frontend Data

React is a component-based JavaScript library for building interactive UIs. The most optimized workflow follows a structure where:

  • ✅ UI is split into reusable components.
  • ✅ Hooks are used for state management & side effects.
  • ✅ Props are used for passing data between components.
  • ✅ Context API or Redux is used for global state.
  • Creating a Functional Component

    A React component is just a JavaScript function that returns JSX (HTML-like syntax).

    export default function MyComponent() {
      return <h1>Hello, React!</h1>;
    }
  • Using Props for Dynamic Content

    Props (properties) allow passing data from **parent to child components**.

    function Greeting({ name }) {
      return <h1>Hello, {name}!</h1>;
    }
    
    export default function App() {
      return <Greeting name="Bob" />;
    }

    ➡ Output: Hello, Bob!

  • Using State with useState

    useState allows components to manage their own state.

    "use client";
    
    import { useState } from "react";
    
    export default function Counter() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increase</button>
        </div>
      );
    }
  • Fetching Data with useEffect

    useEffect is used for fetching data and handling side effects.

    "use client";
    
    import { useEffect, useState } from "react";
    
    export default function DataFetcher() {
    const [data, setData] = useState(null);
    
    useEffect(() => {
    	fetch("https://jsonplaceholder.typicode.com/posts/1")
    	.then((res) => res.json())
    	.then((data) => setData(data.title));
    }, []); // Empty array = run once on mount
    
    return <h1>{data ? data : "Loading..."}</h1>;
    }
  • Handling Forms

    Forms use state to manage input values.

    "use client";
    
    import { useState } from "react";
    
    export default function FormExample() {
      const [input, setInput] = useState("");
    
      function handleSubmit(e) {
        e.preventDefault();
        alert("Submitted: " + input);
      }
    
      return (
        <form onSubmit={handleSubmit}>
          <input value={input} onChange={(e) => setInput(e.target.value)} />
          <button type="submit">Submit</button>
        </form>
      );
    }
  • Lifting State Up

    To share state between components, **lift it up** to the closest common parent.

    function Child({ count }) {
      return <h1>Count: {count}</h1>;
    }
    
    export default function Parent() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <button onClick={() => setCount(count + 1)}>Increase</button>
          <Child count={count} />
        </div>
      );
    }
  • Global State with Context API

    Context API allows sharing global state without prop drilling.

    import { createContext, useContext, useState } from "react";
    
    const ThemeContext = createContext();
    
    function ThemeProvider({ children }) {
      const [theme, setTheme] = useState("light");
    
      return (
        <ThemeContext.Provider value=&#123;&#123; theme, setTheme &#125;&#125;>
          {children}
        </ThemeContext.Provider>
      );
    }
    
    function ThemedComponent() {
      const { theme, setTheme } = useContext(ThemeContext);
    
      return (
        <div>
          <p>Current theme: {theme}</p>
          <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
            Toggle Theme
          </button>
        </div>
      );
    }
    
    export default function App() {
      return (
        <ThemeProvider>
          <ThemedComponent />
        </ThemeProvider>
      );
    }
  • Conditional Rendering

    Use conditional logic to render UI dynamically.

    function ConditionalRender({ isLoggedIn }) {
      return isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please log in.</h1>;
    }
  • List Rendering with map()

    Rendering lists from an array.

    const users = ["Alice", "Bob", "Charlie"];
    
    export default function UserList() {
      return (
        <ul>
          {users.map((user, index) => (
            <li key={index}>{user}</li>
          ))}
        </ul>
      );
    }

Performance Optimization

  • ✅ Use useMemo for expensive calculations.
  • ✅ Use useCallback to prevent unnecessary re-renders.
  • ✅ Use React Suspense for lazy loading components.

Why Use This Workflow?

  • Reusability: Each component is modular and reusable.
  • Performance: State updates are efficient, preventing unnecessary renders.
  • Scalability: Hooks and Context API make it easy to scale apps.
  • Maintainability: Code is clean and structured.

This React workflow follows best practices and works well with Next.js and standalone React apps.

Anything unclear?

Let us know!

Contact Us