Full-Stack Next.js with TypeScript & Shadcn - New API with Frontend Project Checklist

Full-Stack Next.js with TypeScript & Shadcn - New API with Frontend Project Checklist
Amice Wong
4 months, 1 week ago
8 min read
Full-Stack Next.js with TypeScript & Shadcn - New API with Frontend Project Checklist

I use the following checklist to quickstart any new Next.js project. This is my personal, step-by-step guide to go from an empty folder to a working full-stack application..

Step 1: Initial a project

First, let's initialize the project. Open your terminal and use npx to run the latest version of the Next.js setup script. We'll name our project 'closmore'

npx create-next@latest closmore

The installer will ask you a few questions. Choose the options you're most comfortable with (I recommend TypeScript, Tailwind CSS, and the App Router for modern projects).

Once it's finished, navigate into the new directory and open it in your code editor.

cd closmore

In VSCode --> Open New Windows --> to the folder to closmore 

You can now see the basic Next.js project structure.

Let's run the development server to see the default template site.

npm run dev

Visit http://localhost:3000 in the browser.

Click Documentation. Next.js portal contains great documentation on project setup that we may follow.


Step 2: Set Up Git Repository

Before writing any real code, it's crucial to set up version control.

In VS Code, open a new terminal and make sure you are in the project folder. Let's set up a Git repository for this project.

git init
git add .
git commit -m "Initial commit: Closmore Next.js project setup"

Next, go to GitHub and create a new, empty repository (e.g., "closmore"). Then, connect your local repository to the remote one on GitHub.

git remote add origin [YOUR_GITHUB_REPO_URL].git
git push -u origin main

Confirm on GitHub that this initial commit has been pushed successfully.

Step 3: Configure Your Environment

Install Core Dependencies

As your project grows, you'll add more packages. For this project, you might need the OpenAI library later.

npm install openai ......(if any in the phase)

Set Up Environment Variables

M Create a .env file in the root of your project to store sensitive credentials like API keys. For now, we can leave it empty.

Make sure this file is ignored by Git by adding it to your .gitignore file. The default Next.js .gitignore should already include this.

# env file
.env*

Step 4: Create Your First API Route (The Backend)

Next.js allows us to build the backend right inside our project. We'll create a simple "dummy" API endpoint that we can test before building the full frontend.

Create the following folder structure and file:

app/
└── api/
    └── generate/
        └── route.js  <-- This is the API filef

Here is the code for our simple API. It will take a name as input and return a few personalized sentences.

import { NextResponse } from 'next/server';

export async function POST(request) {
  try {
    // 1. Get the data from the request body
    const body = await request.json();
    const { name } = body; //destruct from body

    // 2. Validate the input
    if (!name || name.trim() === '') {
      return NextResponse.json({ error: 'Name is required' }, { status: 400 });
    }

    // .....your future code here....

    // 3. Create the simple, personalized responses
    const responses = [`How are you today, ${name}`, `${name}, you've got it`, `${name} is confident and awesome!`];

    // 3b. Simulate a short delay, like a real API call would have
    await new Promise((resolve) => setTimeout(resolve, 500));

    // 4. Return the successful response as JSON
    return NextResponse.json({ results: responses });
  } catch (error) {
    // Handle any unexpected errors
    console.error('APU Error:', error);
    return NextResponse.json({ error: 'An expected error occured' }, { status: 500 });
  }
}

You can test this endpoint using a tool like Postman to ensure it's working correctly.


Step 5. Build the Minimal Frontend

Now let's build a simple interface to interact with our API. We will edit the main page of our application.

File: app/page.tsx

This is the Frontpage of the webapp. Delete the file's default content. We will no longer use that. Put the following instead.

'use client';
import { useState } from 'react';

export default function Home() {
// 1.  Define all use states
const [name, setName] = useState(''); // State for the input field
  const [results, setResults] = useState([]); // State for the API results
  const [isLoading, setIsLoading] = useState(false); // State to show a loading message
  const [error, setError] = useState(''); // State for any errors

  // 2. Initialize the states and Compose the API request
  const handleSubmit = async (event: any) => {
    event.preventDefault(); // Prevent the form from reloading the page
    setIsLoading(true);
    setError('');
    setResults([]);

    try {
      const response = await fetch('/api/generate', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ name: name }), // It sends the correct JSON
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.error || `Error: ${response.status}`);
      }

      const data = await response.json();
      setResults(data.results); // It expects a "results" key in the response
    } catch (err: any) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  return (
  // 3. Rending HTML
    <main className="p-8 font-sans">
      <h1 className="mb-5">API Infrastructure Test</h1>

      <form onSubmit={handleSubmit}>
        <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter a name (e.g., Amice)" className="p-2 mr-2 border border-gray-300 w-[300px]" required />
        <button type="submit" disabled={isLoading} className="p-2 bg-gray-200 cursor-pointer">
          {isLoading ? 'Generating...' : 'Get Responses'}
        </button>
      </form>

      {error && <p className="mt-4 text-red-500">Error: {error}</p>}

      {results.length > 0 && (
        <>
          <h2 className="mt-5">Results:</h2>
          <div className="p-4 mt-4 border border-gray-300">
            <ul>
              {results.map((result, index) => (
                <li key={index}>{result}</li>
              ))}
            </ul>
          </div>
        </>
      )}
    </main>
  );
}
You can check if it works in the browser under development mode:

npm run dev

Step 6: Adding TypeScript for Robustness

Even it works properly in the browser, you can try building the project like this.
Stop the developer mode by Ctrl-C. Then

npm run build
Errors would be found regarding the undefined types.

This is because Typescript requires requires us to be explicit about our data types. Let's fix this in our page.tsx file, by adding data types for each variable or state.

  // 1.  Define all use states
  const [name, setName] = useState<string>(''); // State for the input field
  const [results, setResults] = useState<string[]>([]); // State for the API results
  const [isLoading, setIsLoading] = useState<boolean>(false); // State to show a loading message
  const [error, setError] = useState<string | null>(null); // State for any errors

  // 2. Initialize the states and Compose the API request
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault(); // Prevent the form from reloading the page
    setIsLoading(true);
    setError(null);
    setResults([]);

Step 7. Styling with Shadcn/ui

To give our app a professional, minimalist style, we'll use shadcn/ui. It's not a component library; instead, it lets you copy and paste beautifully designed components directly into your project.

Make sure your development server is stopped.

7.1. Initialize Shadcn

In your project's root folder, run the init command. It will ask a few questions; the default answers are usually fine.

npx shadcn@latest init

7.2. Install Shadcn components

Next, we'll add the specific components we need for our page.

npx shadcn@latest add button input card

This command creates a new components/ui folder and adds button.tsx, input.tsx, and card.tsx to your project. You can now use these in your code.


7.3. Integrate into app/page.tsx

Now, we'll update our page to use these new components.

Change 1 - Import the components

// --- CHANGE 1: Import React for types and the new components ---
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

Change 2 - Put flexbox for proper alignment within the form

      {/* --- CHANGE 2: Added flexbox for proper alignment --- */}
      <form onSubmit={handleSubmit} className="flex items-center space-x-2">
      {/* <form onSubmit={handleSubmit}> */}

Change 3 - Replace <input> with <Input>

        {/* --- CHANGE 3: Replaced <input> with <Input> --- */}
        <Input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Enter a name (e.g., Amice)"
          className="w-[300px]" // Kept your original width
          required
        />
        {/* <input 
          type="text" 
          value={name} 
          onChange={(e) => setName(e.target.value)} 
          placeholder="Enter a name (e.g., Amice)" 
          className="p-2 mr-2 border border-gray-300 w-[300px]" 
          required /> */}

Change 4 - Replace <button> with <Button>

        {/* --- CHANGE 4: Replaced <button> with <Button> --- */}
        <Button type="submit" disabled={isLoading}>
        {/* <button type="submit" disabled={isLoading} className="p-2 bg-gray-200 cursor-pointer"> */}

Change 5 - Implement Card to the API Result

          {/* --- CHANGE 5: Put the list of response into a Card */}
          <Card className="mt-5">
            <CardHeader>
              <CardTitle>Results:</CardTitle>
            </CardHeader>
            <CardContent>
              <ul>
                {results.map((result, index) => (
                  <li key={index}>{result}</li>
                ))}
              </ul>
            </CardContent>
          </Card>

          {/* <h2 className="mt-5">Results:</h2>
          <div className="p-4 mt-4 border border-gray-300">
            <ul>
              {results.map((result, index) => (
                <li key={index}>{result}</li>
              ))}
            </ul>
          </div> */}

Final version is awesome!


And there you have it! We've built a full-stack API and Frontend application in a single Next.js project, complete with professional styling. You can use it as you boilerplate and further develop with your customized code. Happy Coding!

Great job! Take a coffee break before reading more Amice's articles :P

⁠Simplicity is prerequisite for reliability_Amice_Dev
⁠Simplicity is prerequisite for reliability. ⁠Without clarity, systems become fragile and unpredictable.

Related blogs

Why Build a Blog You Truly Own?  |  Build & Monetize Your Django Blog, Ch. 1
Why Build a Blog You Truly Own? | Build & Monetize Your Django Blog, Ch. 1

By Amice Wong

Read more
Building a Chrome Extension from Scratch — My ClosMore AI Experiment
Building a Chrome Extension from Scratch — My ClosMore AI Experiment

By Amice Wong

Read more
Leveraging BlockNote to Build an Embedded Breathing App in Next.js
Leveraging BlockNote to Build an Embedded Breathing App in Next.js

By Amice Wong

Read more
Building an "On-Demand AI Translator" for My Next.js Blog (Without Monthly Fees)
Building an "On-Demand AI Translator" for My Next.js Blog (Without Monthly Fees)

By Amice Wong

Read more