Introduction: The Developer's Journey to Next.js
Imagine you're learning vanilla JavaScript, moving from one tutorial to another. You finally escaped the tutorial hell and starting to build small projects. You also figure out that vanilla JavaScript is perfect for simple websites or adding a bit of interactivity to existing ones. It's lightweight and works well as long as things stay simple. But as you get better, your projects get more complex and you want to build web apps faster and more efficiently. Managing updates and handling complex state starts to get a bit much. So you look around for a better solution and that's when you discover React.
With React, you learn all about components, State Management, and building dynamic apps that handle user interaction smoothly. You start picking up other tools like Tailwind CSS for styling that makes your workflow even easier. Then you get curious about the backend and want to learn about databases and servers too. You dive into Express.js and work hard to connect it with your React apps. A few months in, you're building full stack apps, but you get a little annoyed by having separate client and server folders each time. Then you start wondering if there was a way to bring them together.
So one day you're watching YouTube and see a new video from Fireship. You just click it immediately because you know it'll be good, and it's about Next.js. While you're watching, you think to yourself that Next.js is exactly what you've been wanting. No more setting up Express separately and you can jump right in because you already know React. Finally, the Best of Both Worlds.
What is Next.js?
So what exactly is Next.js? Next.js is a powerful React framework. It combines everything you love about React with some backend functionality, so you can handle server-side rendering, routing, and API calls all in one place. With Next.js you can easily set up routing without extra libraries like React Router, fetch data server-side and render it before it reaches the user's browser, making your app faster and SEO friendly, create API routes within the same project so you don't need to set up a separate Express server.
Plus, because Next.js is built on top of React, you already know the basics. You're just adding tools that make building and scaling your projects simpler and faster.
Getting Started: Your First Next.js App
So to start using Next.js, open your code editor and type the following in your terminal: npx create-next-app@latest. During installation, you'll see prompts such as the name of your project and other preferences. For this video, we will use the App Router, so select yes for it and press enter. And voila, you've just created your first Next.js web app. To run your Next.js app, simply type npm run dev in your terminal, then go to localhost:3000 in your browser. You'll see the boilerplate that Next.js set up for you.
Now, let's go back to our code editor. You'll notice many files and folders, including Tailwind CSS files, package files, and others. In this setup, the src folder holds everything related to your app's structure and content. This is where you'll find folders like app, which contains all your pages and components, in your global CSS files. If you look closely, there's a JavaScript file called page.js. When you open it, you'll see the boilerplate code that appears when we initially launch our Next.js app. To confirm this, let's remove all the code inside the return function and add a heading with the content "Hello World". This will immediately update the content of our website.
File-Based Routing with the App Router
If you're wondering why this file is called page.js, it's because Next.js uses an App Router where folders define routes. In React, you usually need to download an external library like React Router and then wrap each route inside the home directory, right? But now Next.js makes routing super simple. For example, this page.js we just created is in our app folder, which makes our page.js the homepage, accessible at localhost:3000. But what if we want to go to localhost:3000/about? This is where Next.js shines.
You just need to create a new folder inside the app folder and name it something like about. After that, inside the about folder, create a new file named page.js. This will be the special page serving the about directory content. In simple words, folders represent the name you will see in the URL path, and the page.js file contains the content that will be served. It is important to note that the name page in files is a reserved word for page routes. For example, even if you create a file in the about folder named about.js, Next.js will treat it as a component, not a route.
But what if you want to add more nested routes inside the about directory, like /about/projects and /about/testimonials? Well, you can simply use the same logic we used earlier. Just create a new folder inside the about folder, name it something like projects or testimonials, and inside those folders create their own page.js file. It's as easy as that.
Creating Dynamic Routes
But what if you want to create routes where part of the URL is dynamic, like /about/project/[id]? Let's talk about Dynamic routes. To create Dynamic routes in next.js, you simply use square brackets in the folder. For example, inside the projects folder, you can create a folder called [id]. Then, inside this [id] folder, create the usual page.js file. This will allow you to capture the dynamic parts of the URL as parameters. This way, if you navigate to localhost:3000 and then to /about/project/ followed by any number or word, for example 123, it will serve the page.js file inside the project [id] folder.
Inside the page, we have access to the keyword params. The params object in our code is used to capture Dynamic root parameters in next.js, specifically when dealing with Dynamic routes. In our example, the code is referencing a dynamic route like /about/projects/[projectID]. In this case, projectID is a dynamic part of the URL, and Next.js automatically populates the params object with the corresponding value when the route is accessed. This is super useful when dealing with dynamic fetching. For instance, let's say you want to fetch project details from an API or database based on the project ID. You can use the params object to access the project ID and then make a call to retrieve data for that specific project.
And there you go. Dynamic routes are great for applications with pages for individual entities like blog posts, products, or user profiles where the data changes frequently but the layout or design remains consistent. Without dynamic routes, you would need to manually create a separate page for each entity, leading to a huge number of static pages that are both time-consuming to maintain and hard to scale.
Layouts and Shared UI
After learning about routing in next.js, let's move on to the layout file we see here. In next.js, layouts are a way to share consistent UI across different pages in an application. Just think of layouts as a template for your pages. If you look here, we have a layout called root layout. This file is automatically generated by next.js and if you notice, it takes the children argument. This means that this layout template will apply everywhere, whether in the home directory or the about directory. We can test this by creating a simple Navbar component and placing it above the children inside the body. As you can see it, we automatically have a nav bar even without importing it in the page.js of the home directory.
The best part is if you go to any other pages such as about, this nav bar will also apply to it. The layout file is typically used for elements like nav bars, footers, and sidebars that need to appear on every page. This prevents duplication and ensures that your UI remains consistent. The possibilities are endless.
Above the root layout, we have the fonts and the metadata. Since we are in the layout file, these fonts and metadata will apply to all pages. So if we inspect our website, we can see that the fonts geist mono and geist sans are implemented. Additionally, in the HTML head tag, we can see our metadata descriptions.
If you want to change the font of your app, Next.js has already optimized everything for you. If you didn't know it already, it includes Google fonts. For example, if you want to import Inter as your font, you can get it from next/font/google directory. You just need to create a constant variable and then apply it in the class name. You can also use the global CSS to set the font family.
Up next is metadata. It helps describe, explain, or give context about the web page to both browsers and search engines. This in turn makes the page SEO friendly for search engines, allowing them to index your pages and display them correctly in search results. For example, in the projects page, we wanted this page to have the appropriate title and descriptions and such for the metadata. So to declare a metadata here in the projects page, we can just type export const metadata and inside this object are your title and descriptions. You can also put a list of keywords related to the content of the page and an author. But there's a problem: what if we are dealing with dynamic data like different projects and such, like in the [projectID] folder? You can't just hardcode every metadata, right? That's where Next.js excels, introducing Dynamic metadata.
Dynamic metadata relies on changing information such as current route parameters, external data, or metadata from parent segments. It can be configured by exporting a generateMetadata function that returns a metadata object.
Server vs. Client Components: The Core Concepts
After learning about metadata, let's dive into how Next.js makes rendering incredibly fast. In Next.js, there are two main types of components: Server Components and Client Components. Earlier, we were already using server components because Next.js sets them as the default. In case you didn't know, server components are rendered on the server and sent as static HTML to the client, which improves load times and reduces the JavaScript bundle size. This is ideal for static content that doesn't require interaction, such as headers or data fetched directly from the server. Simply put, server components allows Next.js to deliver content faster by reducing the amount of JavaScript that needs to be loaded on the client side.
On the other hand, when interactivity is required, we can use client components. These are specifically designed for parts of the application that need to manage state or handle user interactions like buttons, forms, or animations. By selectively using client components only where necessary, Next.js optimizes the application's performance while still providing a responsive, interactive user experience. With the combination of server and client components, next JS lets you create highly efficient applications that balance speed and interactivity effectively.
Practical Example: Choosing Between Server and Client Components
Now, let's explore how to decide when to use server or client components in different scenarios. Let's assume we are creating an e-commerce product page in Next.js. Inside the app folder, we create folder for the product and set up the necessary files. Here we have the page that serves the product route and another page that handles the dynamic product ID, plus two components for AddToCart and QuantitySelector. Inside the product ID page, we are fetching product data based on the parameters we get from the dynamic route and displaying them in the return statement. We also generate metadata for SEO optimization.
Since next.js uses server components by default, we offload the rendering of heavy components like data fetching to the server. Because server components render HTML directly, this helps with SEO as search engines can easily index the content by rendering the metadata on the server. Next, we import the quantity selector and add to cart components, passing the params.productID as a prop. Inside AddToCart, we set up a useState hook and a button to console log the product ID we receive. But there's a problem. If we run this project, we get a build error saying "Failed to compile". It says that you're importing a component that uses useState, but this React hook only works in a client component. As we learned earlier, everything that needs interactivity must be rendered in client components. Client components can use state, effects, and event listeners, meaning they can provide immediate feedback to the user and update the UI.
To convert our server component to a client component, simply add "use client" with a double quote on the first line of your file.
Optimizing Apps by Combining Component Types
When server components and client components work together in Next.js, you get the best of both worlds: server-side rendering for fast initial load times and SEO optimization, combined with client-side interactivity for a dynamic, rich user experience. By properly separating concerns, offloading heavy tasks to the server and using the client for interactivity, you ensure both performance and user experience are optimized, leading to faster, more scalable web applications.
If you're still confused about when to use server and client components, here's the entire composition pattern from Next.js, which gives you a clear view of the responsibilities divided between components. After learning about server and client components, as well as how static and dynamic routing work in Next.js, let's now explore how to easily implement API routing in Next.js.
API Routing with Route Handlers
Introducing Route Handlers in Next.js 14. Route Handlers are used in the App Router to define API endpoints, making it easier to handle backend logic directly within your Next.js application. In the app directory, create a folder named after your API route. In our case, this will be called api. Inside the api folder, create a subfolder for the route of your endpoint. For example, imagine we are making a to-do app, so let's create a subfolder named todo-item. Inside the todo-item folder, create a file called route.js. Unlike page routes that respond to HTML content, route handlers allow you to create RESTful endpoints. For instance, if you want to retrieve data for a to-do item from your database, you can use the GET function inside your route file.
Next, let's set up a basic JSON server which will serve as our testing ground. Let's install the json-server library and then create a file named db.json in the root folder. Inside our db.json file, you can put some sample data you want to fetch, like ID, title, and status of the to-do item. Then, let's run the server on Port 5000. You don't have to do this part yourself; we're just setting up a basic server to easily demonstrate how to create, read, update, and delete using route handlers.
Creating and Using a GET Endpoint
To fetch data from the server we created, let's write a fetch function that points to the server's port and the to-do route we set up earlier. Next, let's convert the data to JSON. Finally, when we hit this route, we need to send a response to the user. This is where NextResponse comes in. It will return the to-do data that we fetched.
In the context of an API, a request and a response are two essential parts of communication between a client, such as a web browser or mobile app, and a server, which is the backend application. Requests are typically made in routes such as page.js or specific API route files. Imagine you're at a restaurant. You interact with the wait staff to order food. This process mirrors how APIs work. You tell the waiter what you'd like to eat, which represents the request. This includes details like the order type, perhaps a pizza or salad, similar to an HTTP method like GET to retrieve data or POST to add a new item. You might provide extra details like specific toppings, comparable to the request body that includes necessary data for creating or updating your resource. If you have dietary restrictions, you would inform the waiter, much like headers that provide extra information for the request. The waiter then takes the order to the kitchen, where it's prepared according to your specifications. This represents the processing stage on the server side. When the food is ready, the waiter brings it back to you, completing the response. This request-response cycle enables clients and servers to communicate, allowing the client to get what they need, much like ordering and receiving food at a restaurant.
Fetching Data from an API Route
For our example, NextResponse is the way to send data back to the requestor from us. So if we go to localhost:3000 and then /api/todo-item, which is where our route file is located, this will return the data we receive from the NextResponse. Now, here's where Next.js truly shines. If we create a folder named todos, we can simply access the endpoint we created and get the response data we wanted. Let's create a function called fetchData, then set our URL to the name of the route we created. This will use the GET method, which will call the GET route handler we defined.
Since we're fetching data here, we also need to consider the duration of the request and how often we fetch the data. So let's make our page async, store the response in a variable, and to confirm it's working, let's just console log the data we received from the API. And voila, you've created an API endpoint effortlessly. Of course, for now it may seem challenging because we need to set up various routes like the API directory and data fetching, but with practice, you'll be creating API endpoints as fast as lightning. No need to set up Express.js or anything else.
Creating a POST Endpoint
After learning how to create a get endpoint, how about creating a post endpoint? Well, if we go to the API route file, we can just add a function called POST. A post endpoint is used in APIs to create or send data to the server. When a client sends a POST request to an endpoint, it typically includes data such as form inputs or JSON objects in the request body. To get the request body in our POST function, we can simply pass it as a parameter. Next, assign it to a variable to access the request body and then send the data to the local JSON server we created using the POST method. After that, let's convert the JSON response into a JavaScript object so we can easily work with it in our application. Finally, let's return it using NextResponse.
After creating our POST endpoint, let's use it in our to-do page. Below the fetchData function, we'll create a new async function called postData. Next, we'll give it a parameter called newTodo. Now let's connect to the endpoint we created using fetch with the POST method. The data we pass as the body will be the newTodo parameter, converted to JSON. Finally, let's return the response. Next, in our page component, let's hardcode a new to-do object with an ID, title, and completed status to demonstrate this easily. Then let's use the postData function we created, passing in newTodo and storing the result in a variable so we can log the created to-do to the console.
Of course, if you're using a different database like MongoDB or Supabase, your route handler for GET and POST requests will differ. Try practicing with your own tech stack to master Next.js route handlers.
Conclusion and Next Steps
This in turn will empower you to build full stack applications with ease and exceptional speed. By mastering these concepts, you'll be well equipped to develop dynamic, feature-rich web applications. Next.js offers extensive support for a variety of database libraries, including Supabase, MongoDB, SQL, and more. Additionally, Next.js simplifies authentication by integrating seamlessly with libraries like NextAuth, Kinde, or Clerk, making it easier than ever to secure your applications.
If you're interested in diving deeper into these topics, let us know by leaving a comment in the section below. We'd love to help you learn more. Of course, we won't leave you hanging without resources. We've put in the effort to create a comprehensive PDF guide based on this video detailing everything you need to know to get started with next.js. The guide is completely free, but we truly appreciate your support for the channel. Additionally, we've uploaded the full next.js project discussed in this video to our GitHub with the link provided below.
What we've covered here is just the tip of the iceberg when it comes to Next.js. In upcoming videos, we'll explore how to work with various databases like Supabase and MongoDB, optimize your projects for performance, and even deploy your applications to the internet. There's so much more to learn, so make sure to subscribe and stay tuned for future content. That's all for now, Novas. Thank you so much for watching and we'll see you in the next one.