DEV Community

Cover image for Starting a React API-Driven App? 8 Decisions to Make Before Coding
AppInit
AppInit

Posted on

Starting a React API-Driven App? 8 Decisions to Make Before Coding

Starting a React API-Driven App? 8 Decisions to Make Before Coding

Starting a React project is straightforward.

The harder part often begins after the initial project is created.

For an application that communicates with APIs — such as a dashboard, SaaS frontend, CRM, admin panel, internal tool, or client application — you quickly need to make decisions about:

  • routing
  • API communication
  • server state
  • client state
  • forms
  • validation
  • styling
  • testing
  • project structure

There isn't one combination that every React application should use.

The useful approach is to understand what problem each layer solves and make those decisions deliberately before the codebase becomes difficult to change.

This guide walks through eight areas worth considering when starting an API-driven React application.


1. Start with React + TypeScript + Vite

A practical starting point for a React application is:

React
TypeScript
Vite
Enter fullscreen mode Exit fullscreen mode

Each has a different responsibility.

React

React provides the component-based UI layer of the application.

TypeScript

TypeScript adds static typing to JavaScript. In a growing application, it can make data structures, component props, API responses, and function boundaries easier to understand.

Vite

Vite provides the development server and build tooling around the application.

A minimal project can be created with:

npm create vite@latest my-app -- --template react-ts

cd my-app

npm install
Enter fullscreen mode Exit fullscreen mode

At this point, the project has its basic foundation.

But an API-driven application usually needs several additional decisions.


2. Decide how routing will work

If your application contains multiple screens, routing becomes an important part of the project structure.

For example:

/dashboard
/users
/users/123
/settings
/profile
Enter fullscreen mode Exit fullscreen mode

A routing solution gives the application a consistent place to define:

  • URLs
  • navigation
  • route parameters
  • nested routes
  • layouts
  • route-level UI

React Router is one option for React applications.

A project might organize routing like:

src/
├── routes/
├── pages/
├── components/
└── app/
Enter fullscreen mode Exit fullscreen mode

The exact structure can vary.

The important point is to avoid spreading navigation logic throughout unrelated components.

For a small application, the structure can remain simple.

As the number of routes grows, clearer boundaries become more valuable.


3. Separate server state from client state

This is one of the most important decisions in an API-driven application.

Not all state is the same.

Consider:

Server state

Examples include:

Users
Products
Orders
Posts
Analytics
Notifications
API responses
Enter fullscreen mode Exit fullscreen mode

This information comes from a server and can become stale.

Client state

Examples include:

Sidebar state
Theme preference
Selected filters
UI preferences
Temporary application state
Client-only workflows
Enter fullscreen mode Exit fullscreen mode

This information exists primarily inside the application.

Treating both categories as the same problem can make state management more complicated than necessary.

A useful conceptual separation is:

Server / API state
        ↓
TanStack Query

Client / application state
        ↓
Zustand
Enter fullscreen mode Exit fullscreen mode

This doesn't mean these tools are required.

It means they solve different types of problems.

For example:

React
│
├── TanStack Query
│   └── API/server data
│
└── Zustand
    └── Client/application state
Enter fullscreen mode Exit fullscreen mode

The important architectural question is:

Did this data come from the server, or does it exist only inside the client?

That question can help determine where the state belongs.


4. Decide how API communication is organized

An API-driven frontend will eventually make many requests.

You can use the native fetch API:

const response = await fetch("/api/users");

const users = await response.json();
Enter fullscreen mode Exit fullscreen mode

Or an HTTP client such as Axios:

const response = await axios.get("/api/users");
Enter fullscreen mode Exit fullscreen mode

Neither approach needs to be treated as mandatory.

The more important decision is where API communication lives.

Avoid having components throughout the application directly implement every HTTP request.

Instead, introduce a boundary for data access.

For example:

src/
├── services/
│   ├── users.ts
│   ├── orders.ts
│   └── auth.ts
Enter fullscreen mode Exit fullscreen mode

Then UI components can consume data through application-level functions or hooks instead of knowing how every API request is constructed.

As the number of endpoints increases, this separation can make the application easier to maintain.


5. Decide how forms and validation work

Forms are usually simple in small applications.

They become more complicated when the application contains:

  • login
  • registration
  • profile editing
  • settings
  • filters
  • checkout
  • multi-step forms
  • administrative forms

One practical separation is:

React Hook Form
        ↓
Form handling

Zod
        ↓
Schema validation
Enter fullscreen mode Exit fullscreen mode

React Hook Form manages form state and submission.

Zod can define the expected structure and validation rules for the data.

For example:

import { z } from "zod";

const profileSchema = z.object({
  name: z.string().min(2),
  email: z.email(),
});
Enter fullscreen mode Exit fullscreen mode

The conceptual flow becomes:

User input
    ↓
React Hook Form
    ↓
Zod schema
    ↓
Valid / invalid
Enter fullscreen mode Exit fullscreen mode

This separation can make validation rules easier to locate and reuse.

It also helps avoid scattering validation logic across individual input components.


6. Choose a styling approach early

Styling is another decision that is inexpensive to change at the beginning and potentially expensive to replace later.

Common approaches include:

Plain CSS
CSS Modules
Tailwind CSS
Component libraries
Enter fullscreen mode Exit fullscreen mode

For example, with Tailwind CSS:

<button className="rounded-lg px-4 py-2">
  Save
</button>
Enter fullscreen mode Exit fullscreen mode

The goal isn't to find one styling technology that every project should use.

Instead, consider:

  • project size
  • team preferences
  • design-system requirements
  • component reuse
  • long-term maintenance

Once an application has hundreds of components, changing the styling model can become considerably more work.

That makes the initial choice worth thinking about.


7. Establish testing and code-quality tooling

Testing is often postponed because the first version of an application feels small.

The problem is that testing can become harder to introduce once the application has grown substantially.

A project can establish a basic testing foundation early.

For example:

Vitest
React Testing Library
Enter fullscreen mode Exit fullscreen mode

can provide a starting point for unit and component testing.

Then code-quality tools such as:

ESLint
Prettier
Enter fullscreen mode Exit fullscreen mode

can help keep the codebase consistent.

A basic development workflow can look like:

Write code
   ↓
Type check
   ↓
Lint
   ↓
Test
   ↓
Build
Enter fullscreen mode Exit fullscreen mode

The goal isn't to create a huge CI/CD setup on day one.

The goal is to make quality checks part of normal development before the project becomes difficult to standardize.


8. Decide the project structure before the application grows

A small React project might begin with:

src/
├── components/
├── pages/
├── hooks/
├── utils/
├── App.tsx
└── main.tsx
Enter fullscreen mode Exit fullscreen mode

That may be enough.

As the application grows, a feature-oriented structure can become useful:

src/
├── features/
│   ├── users/
│   ├── orders/
│   └── dashboard/
│
├── components/
├── hooks/
├── services/
├── utils/
└── app/
Enter fullscreen mode Exit fullscreen mode

The important principle is:

Start with the simplest structure that fits the application.

You don't need an elaborate architecture for a small project.

At the same time, don't wait until hundreds of files exist before thinking about boundaries.

A useful evolution can look like:

Small application
      ↓
Simple structure
      ↓
More features
      ↓
Feature-based structure
      ↓
Larger application
      ↓
Stronger boundaries
Enter fullscreen mode Exit fullscreen mode

Architecture should evolve with complexity.


Putting the layers together

For an API-driven React application, one possible starting configuration could be:

React
TypeScript
Vite

React Router
Tailwind CSS

TanStack Query
Zustand

React Hook Form
Zod

Vitest
React Testing Library

ESLint
Prettier
Enter fullscreen mode Exit fullscreen mode

The important part is not the number of dependencies.

Each tool should have a clear responsibility.

┌────────────────────────────────────────┐
│              React + Vite              │
├────────────────────────────────────────┤
│ Routing       → React Router           │
│ Styling       → Tailwind CSS           │
│ Server state  → TanStack Query         │
│ Client state  → Zustand                │
│ Forms         → React Hook Form        │
│ Validation    → Zod                    │
│ Testing       → Vitest / Testing Lib   │
│ Quality       → ESLint / Prettier      │
└────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

This is only an example configuration.

A smaller application might need significantly fewer tools.

For example, an application may not need:

  • a dedicated client-state library
  • a large validation layer
  • extensive testing from the first day
  • a complex architecture

The right question isn't:

How many tools should I install?

It is:

What problems does this application actually need to solve?


A practical decision checklist

Before starting the first feature, ask:

Project foundation

  • Am I using JavaScript or TypeScript?
  • Which build/development tool am I using?
  • Which package manager and Node version does the project require?

Navigation

  • Does the application need multiple routes?
  • How will routes and layouts be organized?

Data

  • Which information comes from an API?
  • How should server state be cached and refreshed?
  • Where will API requests live?

Client state

  • Which state needs to be shared?
  • Can React's built-in state handle it?
  • Do I actually need a dedicated client-state store?

Forms

  • How will form state be managed?
  • Where will validation rules live?
  • Do I need schema validation?

UI

  • Which styling approach fits the project?
  • Do I need a component library?
  • How will reusable components be organized?

Quality

  • How will formatting work?
  • How will linting work?
  • What should be tested?
  • What should pass before a production build?

Making these decisions early doesn't mean locking the architecture forever.

It means creating a clear starting point that can evolve as the application grows.


Final takeaway

A React project is more than:

React + Vite
Enter fullscreen mode Exit fullscreen mode

The real starting point is the set of decisions around it.

For an API-driven application, think in layers:

React
   ↓
TypeScript + Vite
   ↓
Routing
   ↓
API / server state
   ↓
Client state
   ↓
Forms + validation
   ↓
Styling
   ↓
Testing + quality
   ↓
Project structure
Enter fullscreen mode Exit fullscreen mode

You don't need the largest stack.

You need a stack where every piece has a clear responsibility.

Start with the architecture you understand, add tools because they solve real problems, and make the important setup decisions before repetitive configuration starts taking time away from building the application.


AI-assisted content disclosure: This article was prepared with the assistance of AI and reviewed for technical accuracy before publication.

Top comments (0)