- Lab
- Core Tech

Guided: Managing Data in a Next.js Finance Application
In this lab, you will add to existing expense and income pages in a Next.js finance application. This will include supporting CRUD operations within the entire stack. By the end of this lab, you will have a deeper understanding of server components, client components, server actions, API route handlers, and dynamic routes.

Path Info
Table of Contents
-
Challenge
Introduction
Welcome to the Guided: CRUD Operations for a Next.js Finance Application lab.
Throughout this lab, you will learn how to add CRUD operations to a Next.js application using both server-side and client-side rendering. You will build React server components (specific to the Next.js framework) and React client components (a format you may already be familiar with, but if not, do not worry you will review it). Additionally, you will incorporate these components into a Next.js
layout
and learn more about Next.jspages
. By the end of this lab, you should be familiar with API route handlers, React server versus client components, CRUD operations, dynamic routes, and more in a Next.js application.To start, you have been given the bones of a Next.js finance application. The application has expenses, income, transactions (home), and budget planning pages started and accessible on the running application through a basic navigation bar. When you are finished with this lab, users will be able to see, add, delete, and update transactions and budget agenda.
A basic understanding of Next.js’s unique file structure, page and layout interface, and App router may be helpful to complete this lab, but is not necessary. It would also benefit you to have some prior and basic experience with React, JavaScript, HTML, and JSX. Details on Next.js paradigms and design patterns around server and client components and API calls for CRUD operations will be given to provide context for your implementations in each step.
There is a solution directory that you can refer to if you are stuck or want to check your implementations at any time. Keep in mind that the solution provided in this directory is not the only solution, so it is acceptable for you to have a different solution so long as the application functions as intended.
You can launch your application by entering
npm run dev
in the Terminal. The results of your code can be seen in the Web Browser tab if you visithttp://localhost:3000
or visit the following link: {{localhost:3000}} -
Challenge
React Server Components And Client Components
React Server Components and Client Components
Next.js is a full stack framework built on top of React. This means Next.js is capable of hosting React server components in addition to what you know and love, React client components. All components in a Next.js application are React server components by default. So far, all the components in the finance application are React server components since you do not find
’use client’
directive at the top of any component file.Server components are around to improve the developer experience by simplifying work processes like:
-
Page loads! Server components allow page loads to be faster initially because there is no waiting on JavaScript to load. This is because server components are pre rendered at build-time or on revalidation. This will also result in a small client-size JavaScript bundle size since many components are not included on the client-side.
-
Optimizations! Metadata on server components and layouts in Next.js allows for search engine optimization. This is helpful for locating an application or website through Google. Server components also can assist with Image optimizations.
-
Simpler code structure! Server components can access backend services like databases directly while still safeguarding sensitive data like API keys and access tokens that are available on the server. This also means you can create external API calls in server components without worrying about sharing too much information about the client.
However, not all components in Next.js applications can be server components. Since server components are only built at deploy/run time and on revalidation (the process of pulling data in case it has changed), components and pages that require a lot of interactivity and where data changes often are best suited to be client components. This is because you cannot have event listeners like
onClick
oronChange
in a server component. Server components also do not support React hooks likeuseEffect
oruseState
since these hooks trigger prefetching of data and rerenders. When you attempt to use a hook or event listener in a server component, you will get an error notifying you that they only work in a component marked with’use client’
. You can transform any server component to a client component by placing’use client’
at the top of the file in Next.js. Just remember that by changing a component to a client component, you cannot have metadata for that component to help with search engine optimization. -
-
Challenge
Server Component Data Fetching
React Server Component Data Fetching
Since a React server component is built at deploy/run time and has access to the server, data fetching can happen in the component itself. Usually, in a React client component, hooks are used to fetch data and render the page afterwards.
This application will serve for several examples of how Next,js accomplishes data fetching and rendering, so you can plan on the
Expenses
page remaining a server component for the purpose of this lab. You will convert theIncome
page to a client component to compare the two pathways for displaying and fetching data in a future step.The “database” currently lives locally in
app/api
. In each subdirectory, you are provided 2 JSON files calledmockData.json
andoriginalData.json
. ThemockData
file contains initial data and is meant to represent a "database" for storing data and changes made to it will persist across sessions. If you ever want to "reset" the data inmockData
, simply pasteoriginalData
intomockData
. Thetransactions
directory is where you will find transactions of type income, expense, and savings. Thebudget-plan
directory is where you will find budget plans around certain categories of type income, expense, and savings. You can think of the JSON provided in these files as SQL results when asking for the entiretransactions
andbudget-plan
tables.In this step, you will fetch expenses in the
Expenses
page component and display expenses.To begin, create an asynchronous function called
getExpenses
inapp/(ui)/expenses/page.jsx
that returns only the transactions of typeexpense
.Example Answer For Fetching Expenses
async function getExpenses() { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); return transactions.filter((transaction) => transaction.type === "expense"); }
For the above code, do not forget to add the following import:
import { promises as fs } from "fs";
. In the real world, you would have a SQL database and could query the database using SQL in thegetExpenses
method instead of filtering through an array in JavaScript. This is only possible because it is a React server component.Next, you want to display the expenses you can now get through your
getExpenses
method in yourExpensesPage
component. To accomplish this, you will need to make the component asynchronous, await your data fetch, and create a simple html table to show the expenses.Example Answer For Displaying Expenses
export default async function ExpensesPage() { const expenses = await getExpenses(); return ( <div> <h1>Expenses</h1> <table> <tr> <th>Date</th> <th>Amount</th> <th>Category</th> </tr> {expenses.map((expense) => { return ( <tr> <td>{expense.date}</td> <td>{expense.amount}</td> <td>{expense.category}</td> </tr> ); })} </table> </div> ); }
-
Challenge
Client Component Data Fetching
React Client Component Data Fetching
Next.js still supports React client components as usual, and these type of components are what is suggested when a component is going to be very interactive or the data for the component will be changing often. One might suggest the Expenses and Income pages actually be client components since your end goal would be to have these page support all CRUD behavior.
In order to compare the two types of components, you will create an API route handler for getting income transactions, convert the Income component to a client component, and display income transactions on the page in this step.
API Route Handlers are functions that are executed when a user requests an API route, and these are available in your traditional idea of a public backend with API calls. These route handlers deal with incoming HTTP requests for a specific route and respond with the necessary data.
Unlike typical full stack applications that have a separate API, Next.js has the functionality to host API Route Handers within your app structure. You will utilize this functionality by adding an API route handler for getting income transactions.
Instructions (1)
Export an async function named `GET` that returns all transactions of type `income` in `app/api/transactions/route.js`.export async function GET(req) { const file = await fs.readFile( process.cwd() + "/app/lib/transactions/data/mockData.json", "utf-8" ); const transactions = JSON.parse(file); const incomeTransactions = transactions.filter( (transaction) => transaction.type === "income" ); return new Response(JSON.stringify(incomeTransactions)); }
For the above code, do not forget to add the following import:
import { promises as fs } from "fs";
. As mentioned before, if you had a SQL database, you would be performing data queries here.With this method there is an API route handler available at
http://localhost/{port}/api/transactions
where{port}
is the same port that your frontend is running on. You can test this by visiting the url above in the Web Browser and you will see the income transaction JSON.Next, you will want to convert the IncomePage component to a client component so you can use hooks to get the income transaction data available for you at the route created above. To start the conversion, use the
’use client’
directive at the beginning ofapp/(ui)/income/page.jsx
. This tells the Next.js compiler that this page is a react client component.Now, you will write an asynchronous function to get data from the API route handler in
app/(ui)/income/page.jsx
.Example Answer For Fetching Income
async function getIncome() { const res = await fetch("http://localhost:3000/api/transactions"); const incomeJSON = await res.json(); return incomeJSON; }
Lastly, you will display the income transactions served at
http://localhost/{port}/api/transactions
on the Income page.Example Answer For Displaying Income
Since you cannot await the return of the data fetch, you will use a state to maintain the income items. This will allow us to set the items when they are received. As a reminder, asynchronous components are not allowed in client components which is why you are using a hook.export default function IncomePage() { const [incomeItems, setIncomeItems] = useState([]); useEffect(() => { getIncome().then((income) => { setIncomeItems(income); }); }); return ( <div> <h1>Income</h1> <table> <thead> <tr> <th>Date</th> <th>Amount</th> <th>Category</th> <th>Notes</th> </tr> </thead> <tbody> {incomeItems.map((incomeTransaction) => { return ( <tr key={incomeTransaction.id}> <td>{incomeTransaction.date}</td> <td>{incomeTransaction.amount}</td> <td>{incomeTransaction.category}</td> <td>{incomeTransaction.notes}</td> </tr> ); })} </tbody> </table> </div> ); }
Do not forget to add the following import:
import { useState, useEffect } from "react";
.Above, you created a transaction API route handler that returns income transactions. The suggestion is to refactor this end point to take url search parameters to filter to a specific type of transaction (instead of always returning only income transactions). This will create a more versatile end point that aligns closers with RESTful practices.
In
app/api/transactions/route.js
’sGET
method, pull off search params from the request url and gettype
off of them. Instead of filtering transactions by a hard coded string, “income”, compare to the search parameters type.Example Answer to Filtering by Search Param Type
import transactions from "../../lib/transactions/data/mockData"; import { NextResponse } from "next/server"; export async function GET(req) { const { searchParams } = new URL(req.url); console.log(searchParams.get("type")); const incomeTransactions = transactions.filter((transaction) => searchParams.get("type") ? transaction.type === searchParams.get("type") : true ); return NextResponse.json(incomeTransactions); }
You can test this by playing with the
http://localhost/{port}/api/transactions
(where{port}
is the same port that your frontend is running on) and visiting the url iterations in the Web Browser to see different transaction JSON. For example, you can visithttp://localhost/{port}/api/transactions
,http://localhost/{port}/api/transactions?type=income
,http://localhost/{port}/api/transactions?type=expense
andhttp://localhost/{port}/api/transactions?type=none
Update the
IncomePage
component to reach out tohttp://localhost:{port}/api/transactions?type=income
for data. Remember your port is most like 3000, but it is wherever the Next.js application is running locally.Example Answer For New Fetch Address on Income Page
async function getIncome() { const res = await fetch("http://localhost:3000/api/transactions?type=income"); const incomeJSON = await res.json(); return incomeJSON; }
Now that you have a data fetch that happens on the client side, you can add a loading page to delineate that data is being fetched. This is also useful for come server components, but not any of the ones in the finance application yet.
A
Loading
component already exists for you inapp/(ui)/components/loading.jsx
. In yourIncomePage
component add a state to manage when data is loading, and if that state indicates you have not received data, render theLoading
component.Example of Loading State in IncomePage Component
"use client"; import { useState, useEffect } from "react"; import Loading from "../components/loading"; async function getIncome() { const res = await fetch("http://localhost:3000/api/transactions?type=income"); const incomeJSON = await res.json(); return incomeJSON; } export default function IncomePage() { const [incomeItems, setIncomeItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { getIncome().then((income) => { setIncomeItems(income); setLoading(false); }); }); if (loading) { return <Loading />; } … }
You should briefly see the Loading component before the Income table appears now. In a future step, you will see how
loading.jsx
in theapp
directory will automatically show aLoading
component without managing states in React server components, but it is necessary in client components likeIncomePage
. -
Challenge
Creating and Displaying New Transactions
Creating and Displaying New Transactions
You should now have a finance application that will read transactions from a “database” and display two different types on an Expenses page and Income page. By the end of this step, you will also be able to create transactions from these pages and display new transactions as they are created.
Creating Transactions Server Action
You are going to begin by creating a server action that will be used to create a transaction from a form later. Server actions are asynchronous functions executed on the server. They are defined with the React
”use server”
directive at either the top of a function or a file to mark all exports of the file as server actions. Both client and server components can call server actions, however client components can only use module level server actions (server actions exported from a separate file).Finish implementing the
createTransaction
server action inapp/lib/actions.js
. This action will be responsible for adding a transaction to the “database”. In your case, this will look like creating a new transaction fromformData
and usingfs
to write to/app/lib/transactions/data/mockData.json
. The method already fetches all the transactions for you. You may also notice the function accepts a transactionType as an argument, this will come in handy later and you can ignore it for now.Example Answer For Create Transaction Server Action
export async function createTransaction(transactionType, formData) { const transactions = await getTransactions(); const transaction = { id: transactions.length + 1, date: formData.get("date"), amount: formData.get("amount"), category: formData.get("category"), notes: formData.get("notes"), type: transactionType ? transactionType : formData.get("type"), }; transactions.push(transaction); try { await fs.writeFile( process.cwd() + "/app/lib/transactions/data/mockData.json", JSON.stringify(transactions) ); console.log("New transaction successfully added!"); } catch (error) { console.log(`Error adding new transaction: ${error}`); throw new Error("Failed to add transaction"); } }
—
Working to Display New Expense Items
After creating the server action that will create a new expense item, you will render a form component on the Expense page and call the action. The form component you will use already exists in
app/components/server-action-server-component-add-transaction-form
. If you examine the HTML, you will see the<form>
already is calling your server action through theaction
attribute. Import this component in yourExpense
page and call it after the table. On this page, thetransactionType
will be”expense”
and thetransactionPath
will be”/expenses”
.Example Answer For Using the ServerActionServerComponentAddTransactionForm
import AddTransactionForm from "../components/server-action-server-component-add-transaction-form"; … export default async function ExpensesPage() { const expenses = await getExpenses(); return ( <div> <h1>Expenses</h1> <table> … </table> <AddTransactionForm transactionType={"expense"} transactionPath={"/expenses"} /> </div> ); }
Now you should see a form to create expense items when you visit
https:localhost/{port}/expenses
in the Web Browser tab. However, you may notice if you fill out the fields and submit the form, you do not see your new expense item appear.This is because as a server component, the
Expense
page does not rerender unless its cached data “expires”. Server components, that are statically rendered, only cache data in aContent Delivery Network
during build time or revalidation. Revalidation can be used as a tool to basically label the cache as “expired”, leading to a new data fetch. In Next.js, you can revalidate data on demand by revalidating a path.Add the following code in the
createTransaction
server action after writing new data to your “database”. This code will revalidate your expenses and transactions (home) paths. You will also need to add this import at the top of the file:import { revalidatePath } from "next/cache";
.if (transactionPath === "/expenses" || transactionPath === "/") { revalidatePath(transactionPath); }
Visit
https:localhost/{port}/expenses
in the Web Browser tab again. When you submit the form now, you should see the page refresh and be able to locate your new expense.—
Working to Display New Income Items
One disadvantage of server components, like the
Expense
page, is that they have to go through a hard refresh to revalidate and cache new data. However, with a client component you can use states to manage your data and refresh individual components as those states change. TheIncome
page is a client component that is already using state to manage income items. In this step, you will leverage a form component very similar to the last one to create new income items and display them without a hard refresh.In addition to the
ServerActionServerComponentAddTransactionForm
used in the last step, a client version of the same component exists inapp/components/server-action-client-component-add-transaction-form
. SinceServerActionClientComponentAddTransactionForm
is a client component it can use React hooks. If you compareServerActionClientComponentTransactionForm
andServerActionServerComponentAddTransactionForm
you will notice that the client component has asetTransactions
prop and its form action awaits the creation of a new transaction and usessetTrasactions
to update the transaction list. To be frank,setTransactions
is designed to be a setter from theuseState
React hook.Now, use ServerActionServerComponentAddTransactionForm in your
Income
page component.Example Answer For Using the ServerActionClientComponentAddTransactionForm
import AddTransactionForm from "../components/server-action-client-component-add-transaction-form"; … export default async function IncomePage() { const [incomeItems, setIncomeItems] = useState([]); … return ( <div> <h1>Expenses</h1> <table> … </table> <AddTransactionForm transactionType={"expense"} transactionPath={"/expenses"} setTransactions={setIncomeItems} /> </div> ); }
Now you should see a form to create income items when you visit
https:localhost/{port}/income
in the Web Browser tab. When you fill out the form you should notice that an error occurs indicating thatincomeItems
is no longer an array. To fix this error, you must returntransactions
filtered to only yourtransactionType
(or all transactions if a type is not specified) from thecreateTransaction
server action now. Once you add this return statement, you should see your income list update without a hard refresh. -
Challenge
Deleting Transactions
Deleting Transactions
Your finance application will now display transactions and create new transactions. By the end of this step, you will be able to delete transactions from the application.
Deleting Transactions Server Action
Finish implementing the
deleteTransaction
server action inapp/lib/actions.js
. This action will be responsible for deleting a transaction to the “database”. This action takes anid
,transactionPath
, andtransactionType
. The list of transactions is already available for you, you need to find the transaction withid
, delete it, and write the new transaction list to themockData.json
file.Like in the previous step, you will use this server action in the
IncomePage
client component andExpensesPage
server component. To make it so the pages reflect the appropriate transactions in the server components, revalidate theexpenses
andhome
paths after deleting. To allow client components to update state data after calling this server action, return the transactions filtered based on their respective transaction type.Example Answer For Delete Transaction Server Action
export async function deleteTransaction(id, transactionPath, transactionType) { const transactions = await getTransactions(); const dataIndex = transactions.findIndex((item) => item.id == id); if (dataIndex == -1) { console.log(`Transaction with ${id} not found`); return transactions; } transactions.splice(dataIndex, 1); try { await fs.writeFile( process.cwd() + "/app/lib/transactions/data/mockData.json", JSON.stringify(transactions) ); console.log(`Transaction with id ${id} successfully deleted`); } catch (error) { console.log(`Error deleting transaction: ${error}`); throw new Error("Failed to delete transaction"); } if (transactionPath === "/expenses" || transactionPath === "/") { revalidatePath(transactionPath); } return transactions.filter((transaction) => transactionType ? transaction.type === transactionType : true ); }
—
Add a Delete Button to Expenses Table
In the
ExpensePage
component, create another column in the table to display delete buttons. Every row will have a delete button wrapped in a form and the column should have a blank header. The form action will be thedeleteTransaction
server action binded to the row’s expense id, thetransactionPath
“/expenses”
, and thetransactionType
”expense”
.Example Answer For Delete Button Column in Expenses
import { deleteTransaction } from "@/app/lib/actions"; … export default async function ExpensesPage() { const expenses = await getExpenses(); return ( <div> <h1>Expenses</h1> <table> <thead> <tr> <th>Date</th> <th>Amount</th> <th>Category</th> <th></th> </tr> </thead> <tbody> {expenses.map((expense) => { const deleteExpenseById = deleteTransaction.bind( null, expense.id, "/expenses", "expense" ); return ( <tr> <td>{expense.date}</td> <td>{expense.amount}</td> <td>{expense.category}</td> <td> <form action={deleteExpenseById}> <button type="submit">Delete</button> </form> </td> </tr> ); })} </tbody> </table> <AddTransactionForm transactionType={"expense"} transactionPath={"/expenses"} /> </div> ); }
Visit
https:localhost/{port}/expenses
in the Web Browser and delete an expense to test out your new code.—
Add a Delete Button to Income Table
In the
IncomePage
component, create another column in the table to display delete buttons. Every row will have a delete button wrapped in a form and the column should have a blank header.Unlike the
Expenses
page. The delete income transaction form action for theIncome
page will be an asynchronous function that callsdeleteTransaction
with the income transaction id,transactionPath
“/income”
, andtransactionType
”income”
. In this case, you do not have to bind the server action to any parameters because you can call it directly.As in the previous step, also update your income transaction list inside of your form action’s asynchronous function.
Example Answer For Delete Button Column in Expenses
import { deleteTransaction } from "@/app/lib/actions"; … export default async function IncomePage() { const [incomeItems, setIncomeItems] = useState([]); … return ( <div> <h1>Income</h1> <table> <thead> <tr> <th>Date</th> <th>Amount</th> <th>Category</th> <th>Notes</th> <th></th> </tr> </thead> <tbody> {incomeItems.map((incomeTransaction) => { return ( <tr key={incomeTransaction.id}> <td>{incomeTransaction.date}</td> <td>{incomeTransaction.amount}</td> <td>{incomeTransaction.category}</td> <td>{incomeTransaction.notes}</td> <td> <form action={async () => { const transactions = await deleteTransaction( incomeTransaction.id, "/income", "income" ); setIncomeItems(transactions); }} > <button type="submit">Delete</button> </form> </td> </tr> ); })} </tbody> </table> <AddTransactionForm transactionPath={"/income"} transactionType={"income"} setTransactions={setIncomeItems} /> </div> ); }
Visit
https:localhost/{port}/income
in the Web Browser and delete an expense to test out your new code. -
Challenge
Updating a Transaction
Editing an Expense
The last thing you will add to the finance application in this course is the ability to edit an expense.
Editing an Expense Server Action
Finish implementing the
updateExpense
server action inapp/lib/actions.js
. This action will be responsible for saving any expense item updates to the “database”. This action is going to be called from a form so it acceptsformData
as a parameter. Take note of theid
parameter as well for finding the expense in the “database”.Remember to revalidate the
/expense/[id]
path relevant to the updated expense. This will be helpful when you add a form to theExpense
page next.Example Answer For Update Expense Server Action
export async function updateExpense(id, formData) { const transactions = await getTransactions(); const dataIndex = transactions.findIndex((item) => item.id == id); if (dataIndex == -1) { console.log(`Expense with ${id} not found`); return transactions; } const expense = transactions[dataIndex]; const inputData = { date: formData.get("date"), amount: formData.get("amount"), category: formData.get("category"), notes: formData.get("notes"), }; transactions[dataIndex].date = inputData.date; transactions[dataIndex].amount = inputData.amount; transactions[dataIndex].category = inputData.category; transactions[dataIndex].notes = inputData.notes; try { await fs.writeFile( process.cwd() + "/app/lib/transactions/data/mockData.json", JSON.stringify(transactions) ); console.log("Expense successfully updated!"); } catch (error) { console.log(`Error saving expense item}`); throw new Error("Failed to update expense item"); } revalidatePath(`/expenses/${id}`); }
—
Link Expenses on the Expenses Page
On the
Expenses
page, link each expense to its correspondingexpense/[id]
route. Add aDetails
column to the table that contains links to all of the individual expenses using Next.js’sLink
component.Example Answer For Details Column in Expenses
import { deleteTransaction } from "@/app/lib/actions"; … export default async function ExpensesPage() { const expenses = await getExpenses(); return ( <div> <h1>Expenses</h1> <table> <thead> <tr> <th>Date</th> <th>Amount</th> <th>Category</th> <th></th> <th></th> </tr> </thead> <tbody> {expenses.map((expense) => { const deleteExpenseById = deleteTransaction.bind( null, expense.id, "/expenses", "expense" ); return ( <tr> <td>{expense.date}</td> <td>{expense.amount}</td> <td>{expense.category}</td> <td> <Link href={`/expenses/${expense.id}`}>Details</Link> </td> <td> <form action={deleteExpenseById}> <button type="submit">Delete</button> </form> </td> </tr> ); })} </tbody> </table> <AddTransactionForm transactionType={"expense"} transactionPath={"/expenses"} /> </div> ); }
—
Form to Edit an Expense on Expense Page
Create a form in HTML to edit an expense in
app/(ui)/expenses/[id]/page.jsx
. This form should call the server action update expense. Remember to bind the expense id to the server action.Example Answer For Expense Form
export default async function Expense({ params }) { const expense = await getExpense(params.id); const updateExpenseById = updateExpense.bind(null, expense.id); return ( <div> <form action={updateExpenseById}> <input name="date" type="date" defaultValue={expense.date} required /> <input name="amount" type="number" step="0.01" defaultValue={expense.amount} required /> <input name="category" type="text" defaultValue={expense.category} required /> <input name="notes" type="text" defaultValue={expense.notes} /> <button type="submit">Update Expense</button> </form> </div> ); }
For reference, individual or dynamic routes are denoted in Next.js by a folder name enclosed in square brackets. This is what makes it possible to replace
id
with a specific expense id available and you can access it in the server or client component on theparams
prop.Hooray! You now can edit expense items in the finance application.
-
Challenge
Looking Forward
In this lab, you flushed out the
Expenses
andIncome
pages with data. This required creating Next.js server actions and route handlers to perform data operations, writing HTML and JSX to show data in tables, and learning how to execute server code in client and server components.By now, you should be familiar with the difference between server and client components as well as the advantages and disadvantages of each type of component. You also should have knowlegde about Next.js server actions, API route handlers, and how to visit nested routes using
Link
.The finance application is coming along, and in the
Guided: Daisy UI and Tailwind CSS to Style a Next.js Finance Application
you will take off from where this lab left off to create a cohesive design for the application. The upcoming lab will also cover Next.jsStreaming
, Image and Font optimization, static vs dynamic rendering, and accessibility.
What's a lab?
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Provided environment for hands-on practice
We will provide the credentials and environment necessary for you to practice right within your browser.
Guided walkthrough
Follow along with the author’s guided walkthrough and build something new in your provided environment!
Did you know?
On average, you retain 75% more of your learning if you get time for practice.