How to Build a Scalable React App with Next.js and TypeScript

By Adam Hultman

DevelopmentNextJSTypeScriptReact
How to Build a Scalable React App with Next.js and TypeScript

Setting Up Next.js with TypeScript

Getting started with Next.js and TypeScript is straightforward, but a few tweaks can save you a lot of headaches down the road. The official documentation makes it easy, but here’s a quick refresher:

First, let’s create a new Next.js app with TypeScript:

1 npx create-next-app@latest my-scalable-app --typescript

This command sets up a new Next.js app with TypeScript configured out of the box. If you already have an existing Next.js project and want to add TypeScript, just add a tsconfig.json file at the root, and Next.js will automatically configure it for you.

The benefit of using TypeScript with Next.js isn’t just the type safety (though that’s a big one). It also helps with catching bugs early, providing better IntelliSense in editors like VS Code, and making your code more self-documenting. But make no mistake—TypeScript can feel like a pain if you dive in too deep without understanding the basics, so take your time getting used to it.


Organizing the Project Structure for Maintainability

As your app grows, having a well-organized project structure becomes crucial. It’s tempting to just drop everything into a single pages directory, but that can get messy fast. Here’s a structure that has worked well for me:

1 2 3 4 5 6 7 8 9 10 /src /components /hooks /layouts /pages /services /utils /styles /context /types

This setup keeps related logic together and makes it easy to find what you need. Here’s a quick rundown:

  • /components: Reusable UI components like buttons, modals, or form elements.
  • /hooks: Custom hooks for things like data fetching, handling form states, or other reusable logic.
  • /layouts: Components that define the overall structure of your pages (e.g., MainLayout for the default layout).
  • /pages: Next.js-specific routing files. This is where your actual page components go.
  • /services: API-related logic, such as functions that handle HTTP requests using fetch or axios.
  • /utils: Utility functions that don’t belong anywhere else, like date formatters or local storage helpers.
  • /context: Context providers for global state management, if you’re using React Context.
  • /types: TypeScript types and interfaces, organized by feature or module.

By separating out the logic into these folders, you can keep your components lean and your pages focused on rendering. This structure is flexible enough to grow with the project and makes onboarding new team members easier—they can find what they need without a scavenger hunt.


Managing API Routes and Integrating with External Services

One of the best parts about Next.js is its built-in support for API routes. If you’re coming from a Create React App setup, this feels like a game changer. It allows you to create serverless functions right alongside your frontend code.

For example, say you’re building a feature that fetches blog posts from an external API. You can create an API route like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // /src/pages/api/posts.ts import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'GET') { res.status(405).json({ message: 'Method not allowed' }); return; } try { const response = await fetch('https://api.example.com/posts'); const posts = await response.json(); res.status(200).json(posts); } catch (error) { res.status(500).json({ message: 'Error fetching posts' }); } }

This approach keeps your API logic close to the components that use it. It also helps when you want to protect certain routes, like adding authentication checks before fetching data.

But remember: these API routes are still serverless functions, so try to keep them light. If you need to do something more complex, consider using a dedicated backend like Node.js with Express or deploying a separate server with a service like AWS Lambda.


Strategies for State Management and Data Fetching

State management in a scalable React app can get tricky, especially when you have a mix of local state and data that needs to be shared across components. For most apps, you’ll need a combination of React’s built-in useState and useReducer for local state, React Context for global state, and a library like React Query or SWR for data fetching.

React Query is my go-to for data fetching, caching, and synchronizing server state. It simplifies things like pagination, background refetching, and managing the loading state:

1 2 3 4 5 6 7 8 9 10 11 import { ueQuery } from 'react-query'; const usePosts = () => { return useQuery('posts', async () => { const response = await fetch('/api/posts'); if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }); };

With React Query, you don’t need to write much boilerplate code for handling loading, error states, or caching. It also automatically refetches data when the user revisits a page or regains network connection.

For global state, React Context works well when you have a small number of global values, like user authentication status. But if your app has more complex state needs, you might want to look into Zustand or Redux Toolkit. Both offer more structure and are better suited for large-scale apps.


Deployment Tips for Vercel or AWS

Next.js is optimized for deployment on Vercel, but you can easily deploy it on AWS using services like Amplify or Elastic Beanstalk if you need more control. Here’s what you should keep in mind for each:

Vercel: It’s by far the easiest option, especially since Next.js was created by the team behind Vercel. Deployment is as simple as pushing your code to GitHub—Vercel will automatically build and deploy your site. It’s also great for features like server-side rendering and API routes, which Vercel handles out of the box.

AWS Amplify: If you’re already using AWS for other parts of your stack, Amplify can be a good fit. It takes a bit more setup than Vercel, but it offers more flexibility in terms of managing backend services like databases and authentication through Cognito.

In either case, don’t forget to set up environment variables for things like API keys and database URLs. Vercel and AWS Amplify both support this through their dashboards, allowing you to manage secrets securely.


Performance Optimizations for a Better User Experience

Performance is often the difference between a decent app and one that users actually enjoy using. Next.js makes some optimizations for you, but there are a few things you should consider:

  • Image Optimization: Use Next.js’s Image component, which automatically optimizes images for different screen sizes and formats. It’s one of the easiest ways to shave off load time, especially for media-heavy pages.
  • Static Site Generation (SSG) and Incremental Static Regeneration (ISR): If you have pages that don’t change often, like blog posts or product pages, use SSG. This allows Next.js to generate the HTML at build time. For content that updates occasionally, use ISR to regenerate the page at a specified interval, keeping it up-to-date without slowing down initial page loads.
  • Code Splitting: Next.js does this automatically for pages, but if you have large components or libraries, consider lazy-loading them using React.lazy and Suspense:
1 2 3 4 5 6 7 8 9 const SomeHeavyComponent = React.lazy(() => import('./SomeHeavyComponent')); export default function Page() { return ( <Suspense fallback={<div>Loading...</div>}> <SomeHeavyComponent /> </Suspense> ); }

This way, you can load heavy components only when they’re needed, reducing the initial bundle size.


Building a scalable React app with Next.js and TypeScript takes some planning, but the payoff is worth it. By keeping your codebase organized, leveraging the power of TypeScript, and using Next.js’s built-in optimizations, you can create an app that not only performs well today but is also ready to grow with your needs. Whether you’re building a side project or a production-grade application, these best practices will help keep you on the right track.


© 2024 Adam Hultman