Skip to main content
Back to home

Building Modern Web Applications with Next.js and React

· 5 min read
Owen Hayes
Owen Hayes
Web3 Engineer @CatFee

Technology Stack

As frontend technology advances rapidly, building efficient, modern web applications is increasingly important. This article presents a complete development approach using Next.js, React, shadcn, TailwindCSS, Zod, Zustand, React Query, and openapi-react-query.


1. Technology Stack Overview​

Next.js​

A React-based framework supporting server-side rendering (SSR), static site generation (SSG), and full-stack development. It improves SEO capabilities and optimizes performance.

React​

A modern, component-based frontend library and a core tool for building user interfaces.

shadcn​

A component toolkit combining Radix UI and TailwindCSS, offering a broad selection of foundational UI components.

TailwindCSS​

A utility CSS framework for quickly building responsive, modern interfaces through class names.

Zod​

A type-safe validation library for validating form data or API request data, with strong TypeScript integration.

Zustand​

A lightweight state management tool suited to complex but localized application state.

React Query​

A server state management tool that handles data fetching, caching, synchronization, and updates.

openapi-react-query​

Generates type-safe React Query API clients from OpenAPI specifications, improving development efficiency and safety.


2. Quick Project Setup​

2.1 Create a Next.js Project​

Initialize a project with TypeScript:

npx create-next-app@latest my-nextjs-app --typescript
cd my-nextjs-app

2.2 Install Dependencies​

Install the core libraries in the stack:

npm install tailwindcss postcss autoprefixer zustand @tanstack/react-query zod openapi-react-query
npm install @shadcn/ui

2.3 Configure TailwindCSS​

Initialize the TailwindCSS configuration file:

npx tailwindcss init

Update tailwind.config.js:

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

Import TailwindCSS in styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

2.4 Integrate React Query​

Create a React Query client:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

export default function App({ Component, pageProps }: any) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}

2.5 Configure openapi-react-query​

Generate an API client from the OpenAPI specification:

npx openapi-codegen generate --input ./path-to-openapi.json --output ./generated --client react-query

Provide the API configuration in the project:

import { createClient, ApiProvider } from "./generated";

const apiClient = createClient({
baseUrl: "https://api.example.com",
fetch: async (url, options) => {
// Add authentication here, such as a token in the request headers
const token = "your-auth-token";
return fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
},
});

export default function App({ Component, pageProps }: any) {
return (
<ApiProvider client={apiClient}>
<Component {...pageProps} />
</ApiProvider>
);
}

3. Form Development with shadcn and Zod​

Using a registration form as an example, the following demonstrates how to combine shadcn and Zod to build a type-safe, user-friendly form.

3.1 Define Validation Rules​

import { z } from "zod";

const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
});

export type UserFormValues = z.infer<typeof userSchema>;

3.2 Build the Form Component​

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";

export default function UserForm() {
const form = useForm<UserFormValues>({
resolver: zodResolver(userSchema),
defaultValues: {
name: "",
email: "",
password: "",
},
});

const onSubmit = (values: UserFormValues) => {
console.log("Submitted values:", values);
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
name="name"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Your email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Your password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Register</Button>
</form>
</Form>
);
}

4. State Management with Zustand​

Manage application state, such as global user data, with Zustand:

import create from "zustand";

interface UserState {
user: { name: string; email: string } | null;
setUser: (user: { name: string; email: string }) => void;
}

export const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
}));

Use Zustand in a component:

import { useUserStore } from "@/store/user";

export default function Profile() {
const user = useUserStore((state) => state.user);

return (
<div>
{user ? <p>Welcome, {user.name}!</p> : <p>Please log in.</p>}
</div>
);
}

5. Conclusion​