Before we look more closely at the code, let's check out the Figma designs for this todo app. As a developer, you'll need to know the basics of navigating Figma projects and translating designs into code. Follow the steps below to familiarize yourself with Figma, and feel free to explore further!
-
Open the Onboarding Todo App Figma project.
-
If you're not signed in already, click "Log in" in the top right corner. Once you're signed in, you should see "View only" in the center of the top bar.
-
Move the viewport: Scroll or middle-click and drag to pan around. Pinch (trackpad) or scroll while holding
Ctrl
to zoom in and out. -
Select an element: Click on an element in the main viewport or left sidebar to select it.
- Generally, Figma only lets you select an element if its parent (such as a frame or group) is currently selected, which can be unwieldy for deeply nested elements. To select an element directly, right-click on it in the viewport and choose "Select layer," then choose the element you want to select.
-
Navigator panel: The left sidebar has two sections: a page selector and a tree navigator.
- You should start out on the "Part 1" page, which contains all the designs we need for Part 1. Click on "Part 2" to view the Part 2 designs.
- In the tree navigator, you can click on an element to highlight it in the main viewport or click on the triangle next to it to show/hide its children.
-
Properties and Export panels: The right sidebar has three tabs at the top: Comment, Properties, and Export. We generally don't need to add comments as developers, so click on one of the other two tabs to see those panels.
-
Properties: This panel shows style information from the currently selected element. If no element is selected, it shows a list of preset text and color styles which the designers have chosen.
❓ Hint: Style values
As of writing, in view-only mode this list doesn't actually tell you anything about the style values (color, font size, etc.), so to see those, you can either look at the properties of individual elements or ask your designers to write out all the values somewhere. For this todo app, all colors and font styles are defined already in the frontend starter code (see below), so you'll just need to match them to things in the Figma designs.
-
Export: This panel allows you to export any element as an image. We'll use this while developing the todo app, and it's likely that your future TSE projects will also have some custom icons or graphics which you'll want to export. Follow the steps below to export an element.
- Select the element.
- In the Export panel, click the + next to the element you want to export.
- Choose a file type (PNG, JPG, SVG, or PDF). SVG is the best in most cases because it can display at any size.
- Click Export.
-
Finally, let's examine the structure of the project and some important files. Here we stay specific to MERN, but you'll see similar patterns across projects inside and outside of TSE, even those that use other tech stacks. Most of the files also have comments explaining what each part of the code does in greater detail—we encourage you to read through those as well.
As we've seen, the repo has two main folders: backend
and frontend
. Note that this is just one way to organize a full-stack project. Some projects place the frontend code inside the backend directory, structure the backend route handlers differently, place frontend style and test files in their own folders, etc. It doesn't really matter what convention you follow, as long as you stay consistent within each project.
In a deployed project, the frontend and backend are often hosted on their own servers with their own Node environments. During development, we simulate this by placing them in separate directories and running each Node environment on different ports.
-
package.json
marks this folder as a Node package and provides information so Node knows how to run the code. As a JSON file, it's formatted as a set of"key": <value>
pairs. Most of the keys would only be relevant if we were publishing this as a library on NPM, so we'll just cover the ones that are important for us as developers. (package.json
documentation)Contents of package.json
dependencies
: Set of packages that we use in our own code and the allowed version numbers. NPM refers to this when it installs dependencies (npm install
), and it automatically updates it when we add another package (npm install <package>
). See thepackage.json dependencies
docs for more info about the syntax.devDependencies
: Set of packages that we use only during development (such as testing frameworks and documentation generators) and the allowed version numbers. If you runnpm install --save-dev <package>
, it adds the package todevDependencies
. There's not really a difference betweendependencies
anddevDependencies
, except if we publish the package to NPM and someone else uses it. So, we recommend just adding packages todependencies
unless there's a special reason to usedevDependencies
. See thepackage.json devDependencies
docs for more info.scripts
: Set of scripts we can run withnpm run <script-name>
. We can write arbitrary commands for Node to run as part of each script. For example, thelint-check
script is from our linting repo and runs ESLint to scan our code for inconsistencies. Some special scripts run at preset times—for example,prepare
runs automatically after everynpm install
, andstart
runs when we runnpm start
. See the NPM scripts docs for more info._moduleAliases
: Map of module aliases to the corresponding file paths. We use this so we can write simpler import paths (such as "src/foo" instead of "../../src/foo"). This is custom behavior provided by the module-alias package.
-
package-lock.json
keeps track of the version of each dependency which was last installed. This file is often quite large, and npm automatically updates it so there's generally no need to edit it manually. However, it's still very important to include it in Git because it ensures that different developers are using the same packages, thus avoiding conflicts. -
node_modules
contains the code from all our dependencies. This folder is also often very large, and you generally won't need to look at anything inside it. Sincepackage.json
andpackage-lock.json
have all the information that NPM needs to keep track of dependencies, we usually have Git ignore thenode_modules
folder. -
src/server.ts
is the entry point of our backend code, meaning Node runs this file first. In this file, we connect Mongoose to our MongoDB instance and start up our Express middleware. -
src/app.ts
configures Express to call our request handling code and sets up a generic error handler. -
src/models
stores the Mongoose schemata (plural of schema) that we use in our database. In general, a schema defines the expected "shape" of an object, including its field names and their data types. The starter code only has one type of object, a Task, whose schema you can find insrc/models/task.ts
. -
src/routes
contains our Express route definitions. Each route is a specific path where the frontend can send an HTTP request to trigger the corresponding handler. -
src/validators
contains our server-side validation logic. These validators check each request to ensure that its data have the correct format before any other route handling logic runs. If the format is correct, then the data get passed along to the route handler; if not, then it redirects to an error handler. -
src/controllers
contains the request handler (a.k.a. route handler) code. This is where the real business logic happens on the backend.
🤔 For new developers: How Express runs this code
Let's trace the code path from src/server.ts
to src/controllers/task.ts
to understand how Express runs this code.
- In
server.ts
, we callapp.listen()
with two arguments: the port number and a callback function to run once Express has successfully initialized.app
is imported fromapp.ts
. - In
app.ts
, we set up Express by callingconst app = express()
, then we callapp.use("/api/task, taskRoutes")
. The latter tells Express that for any HTTP request whose path starts with/api/task
, the handler code is intaskRoutes
, imported fromroutes/task.ts
. - In
routes/task.ts
, we create anexpress.Router
to handle those/api/task
routes. Each handler specifies a route suffix and one or more functions to call when handling requests on that route. Those functions come fromcontrollers/task.ts
andvalidators/task.ts
.router.get("/:id", TaskController.getTask)
handles requests toGET /api/task/:id
(where:id
is the ID of a Task object) by calling thegetTask
controller. Therouter.delete
call works very similarly.router.post("/", TaskValidator.createTask, TaskController.createTask)
handles requests toPOST /api/task
by calling thecreateTask
validator and controller.
- As we said above, the functions in
validators/task.ts
check that a Task object in a request has the correct fields and data types. - The functions in
controllers/task.ts
interact with the database, such as by creating new documents with the Task schema. That schema is imported frommodels/task.ts
. - In
models/task.ts
, we define a Task object to have a required stringtitle
, optional stringdescription
, optional booleanisChecked
, and required DatedateCreated
. MongoDB will automatically give each object its own unique ID in a field called_id
.
-
package.json
, like the one in the backend folder, denotes a Node package. Similarly, we also have apackage-lock.json
file and anode_modules
folder. -
index.html
contains the actual HTML document where React injects all of its generated HTML in the browser. Since we write all our pages and components in React, this is the only HTML file needed in the entire project. The file itself was generated by Vite, and we added some extra code to download the Rubik font from Google Fonts and tell the browser where the site favicon is located. -
src/main.tsx
is our frontend's entry point. It tells React where to inject our content inindex.html
and renders theApp
component fromsrc/App.tsx
. -
src/App.tsx
is the root of our React application code, where we set upreact-router
with our frontend pages. Note that the "routes" in this file are different from our backend API routes—here, we map each page to its own URL for the purpose of client-side routing. In general, the only other code inApp.tsx
should be things that we want to make available to all parts of the application. -
src/pages
stores the pages of our frontend. We currently have two pages: Home and About. The Home page displays a form for creating new tasks, while the About page contains some simple text (mainly to demonstrate how to link to another page).✅ Good practice: Unique page URLs
Now that the industry has more advanced frontend technologies like React instead of raw HTML/CSS, the concept of a website "page" is a little more fluid than before. For instance, one React JavaScript file can dynamically render multiple different pages under the same URL. We still recommend mapping pages and URLs one-to-one for the purpose of keeping certain functionalities, like bookmarking pages.
-
src/components
contains code for our React components. Each component is a reusable element of the frontend, such as a button, popup, or search bar. We write our components in JSX, which is an extended, React-specific version of JavaScript/TypeScript that allows us to write HTML-like code directly in our JS/TS files.🤔 For new developers: Components and modularity
The idea of components isn't unique to React, but it's definitely one of its defining characteristics. By writing different parts of our frontend into components, we make them more modular—easier to think about, reuse, and extend. Also, the "pages" of our application are technically React components too, but it's more helpful to separate them as a different type of abstraction.
Contents of src/components
Button
andTextField
are the smallest and most reusable components. Because we'd expect these kinds of elements to be used in a lot of different ways, they are written generically, with lots of options. See the Figma file for illustrations of each component's variations.HeaderBar
is also a small component, but because we know it will only be used in one way (at the top of every page), we don't need to add any options.TaskForm
is the task creation form that you see on the Home page. It uses bothButton
andTextField
. Note that this component contains both rendering logic (what gets displayed to the user) and some business logic (performing some operations when the user clicks Save). This is common in medium-to-large components—as the size of the component increases, so does the amount of logic it encapsulates.- The CSS files (ending in
.css
) in this folder provide styles to our components, including colors, fonts, borders, positioning, etc. We actually use CSS Modules (hence the.module.css
; automatically enabled with CRA) so that the styles in each file only get applied to components that specifically import those styles. - The test files (ending in
.test.tsx
) in this folder define automated unit tests for each component, which are helpful for preventing regressions (unintentionally breaking things when we make changes). We use Vitest and React Testing Library to run these tests. There is only one test file in the starter code,TaskForm.test.tsx
, because that's the only component with enough complexity to merit testing. In TSE, we generally encourage automated testing of medium-complexity components and helper functions, including on the backend if worthwhile.
-
src/api
contains our frontend API client—a set of helper functions that send HTTP requests to our backend routes, which we discussed above, and parse the responses. No frontend page or component should send any HTTP requests directly; instead, they should call these functions. We recommend gathering all the API request code in one place like this to make it easier to understand and maintain. -
src/globals.css
contains the application's global styles. This includes things like app colors and fonts, common text styles, and other styles that we want to apply to everything in the application. We can also override these styles with more specific CSS Modules stylesheets.
With that massive exposition dump over, take a deep breath. You made it, now let's start coding!
Previous | Up | Next |
---|---|---|
0.2. Clone and run the project | Part 0 | Part 1. Minimum viable product |