Introduction
Here you will find useful information for configuring a development environment and for completing the training program.
Here you will be able to find more information about how we work and other useful information.
Feel free to ping anyone on the team if you think something can be added/improved in these guides.
Setup your machine
Code Editor
Choose your code editor
Time to configure it
-
If you chose Cursor/VS Code
-
Create
codecommand -
Open the Command Palette
(⇧⌘P)and type shell command to find the Shell Command: Install 'code' command in PATH command. Restart the terminal for the new$PATHvalue to take effect. You'll be able to typecode .in any folder to start editing files in that folder. -
Configure user settings
(⇧⌘P)and type user settings and select Preferences: Open User Settings Depending on your VSCode version you will see a JSON view of the settings by default or not. If it doesn't show at first you can click a{}button on the top right that will show you the settings JSON view.{ "explorer.confirmDelete": false, "editor.scrollBeyondLastLine": false, "editor.wordWrap": "on", "files.trimTrailingWhitespace": true, "editor.tabSize": 2, "markdown.preview.scrollPreviewWithEditor": false, "[python]": { "editor.tabSize": 4, }, "files.insertFinalNewline": true, "markdown.preview.scrollEditorWithPreview": false, "html.suggest.html5": false, "editor.quickSuggestions": { "other": true, "comments": false, "strings": true }, "window.zoomLevel": 0, "python.venvPath": "~/.virtualenvs", } `
-
-
If you chose Sublime
-
Create
sublcommandsudo mkdir -p /usr/local/bin/ && sudo ln -s /Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl /usr/local/bin/subl -
Install package manager as shown here
-
Configure user settings (
Sublime Text -> Preferences -> Settings){ "ensure_newline_at_eof_on_save": true, "font_size": 14, "show_encoding": true, "tab_size": 2, "translate_tabs_to_spaces": true, "trim_trailing_white_space_on_save": true }
-
General MAC OS config
-
Enable "Show Path" bar in Finder
defaults write com.apple.finder ShowPathbar -bool true -
Enable "Show Status" bar in Finder
defaults write com.apple.finder ShowStatusBar -bool true
Xcode
Xcode is an IDE for macOS developed by Apple for developing software for macOS, iOS, iPadOS, watchOS, and tvOS.
- Install Xcode from the App Store
- Open Xcode and accept the licence
- Install command line tools with this command:
xcode-select --install
Homebrew
Homebrew is a free and open-source software package management system that simplifies the installation of software on Apple's macOS operating system and Linux.
Installation
-
To install it, run in Terminal the following command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -
Run
brew doctorand update homebrew withbrew update
Git
Git is a distributed version-control system for tracking changes in source code during software development. It is designed for coordinating work among programmers, but it can be used to track changes in any set of files.
Installation
Xcode developer tools already installs GIT for us.
Configuration
-
If migrating from another computer, import previous configuration following this guide
-
If no configuration was created, execute these commands in your terminal:
ssh-keygen -t rsa -C "YOUR EMAIL" git config --global user.email "YOUR EMAIL" git config --global user.name "YOUR NAME" git config --global color.ui true
Useful tips
GitHub
GitHub is a web-based collaborative software platform for software development version control using Git. It provides access control and several collaboration features such as bug tracking, feature requests, task management, and wikis for every project.
If you are new to GitHub, create an account.
When you finished, give your account username to somebody in the team
wget
GNU Wget is a computer program that retrieves content from web servers. It supports downloading via HTTP, HTTPS, and FTP.
Installation
Run the following command:
brew install wget
Postgres / PostgreSQL
Postgres is a free and open-source relational database management system emphasizing extensibility and SQL compliance.
Installation
You can install it from Postgres app page
Configuration
Configure PATHs
-
Open
zshrc:nano ~/.zshrc -
Paste the following:
export PATH=$PATH:/usr/local/bin export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin -
Save those changes and exit.
-
Apply those changes:
source ~/.zshrc -
Finally, type
psqlin your terminal to open Postgres console and run:CREATE USER postgres SUPERUSER;
Beekeper Studio
Beekeeper Studio serves as a more user-friendly, polished alternative to command-line SQL or some more complex database tools.
Because it supports multiple database types, it can be a single interface for managing multiple databases.
Install it from here.
Ruby (Rails team only)
Ruby is an interpreted, high-level, general-purpose programming language.
We use RVM (Ruby Version Manager) to easily install, manage, and work with multiple Ruby environments from interpreters to sets of gems.
Installation
Run the following command:
curl -sSL https://get.rvm.io | bash -s stable --rails
source ~/.rvm/scripts/rvm
Once RVM is installed, install latest Ruby version:
rvm use ruby --install --default
Configuration
Configure no documentation for gems (speeds up gem installation process):
echo 'gem: --no-document' >> ~/.gemrc
Install rails and bundler
gem install rails bundler
Node.js (Node team only)
Node.js is an open-source, cross-platform, JavaScript runtime environment that executes JavaScript code outside of a web browser.
nvm (node version manager)
We use nvm to manage different Node.js versions between projects. Run the following command:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
Check that everything went well running the following command:
command -v nvm
pnpm / bun
pnpm and bun are package managers for the JavaScript programming language. Install either of those.
Python (AI team only)
Python is an interpreted, high-level, general-purpose programming language. Python 3 is the latest version of this language.
Installation
We use uv to manage python versions and dependencies. Install it:
curl -LsSf https://astral.sh/uv/install.sh | sh
echo 'eval "$(uv generate-shell-completion zsh)"' >> ~/.zshrc
echo 'eval "$(uvx --generate-shell-completion zsh)"' >> ~/.zshrc
Configuration
-
Setup Python's environment management
Install a specific Python version
uv python install 3.12List installed versions
uv python list -
To create a Python3 virtual environment
It's usual to have different Python environments for each project. This keeps the project dependencies independent from each other.
Create an environment:
uv venv .venvActivate it:
source .venv/bin/activate -
To exit the virtualenv
deactivate
Troubleshooting
If you experienced errors while bundling, run this commands in Terminal:
gem uninstall libv8
brew install v8
gem install therubyracer
gem install libv8 -v '3.16.14.3' -- --with-system-v8
Extra (optional)
Docker
Docker is a set of platform as a service products that uses OS-level virtualization to deliver software in packages called containers.
Installation
You can install Docker Desktop app from docker page.
Training program
This training program will help new developers learn all the basic concepts and technologies that we use on a daily basis. We are going to start learning GIT. Then, we are going to learn HTML, then a little of CSS and JS. After that, we will start learning how to implement a web server. We will also learn how to use databases and finally we will dive into Ruby on Rails (RoR) or Node.js.
We will start a small project and improve it at each step.
Please feel free to make any recommendations to improve it.
Common section
This section contains all the common parts of the trainging program.
The Unix Shell
There are multiple ways of interacting with a computer. In general, operating systems use either a graphical user interface (GUI) with a mouse, or a command-line interface (CLI) with just a keyboard.
The shell is just a program that exposes the operating system's services to a person or other programs via a CLI. As you might imagine, there are multiple implementations of the shell program. The two most common nowadays are bash and zsh (Linux usually uses bash while MacOS uses zsh, which is pretty similar to bash).
Think of the shell as both a command line interface and a scripting language, allowing you to automate repetitive tasks if needed.
The shell is one of the most powerful tools a developer can have. Once you master it, it will allow you to perform mundane task (such as viewing the contents of a directory) and really complex task, such as copying files to a remote computer in another part of the world.
Basics
If you don't have experience with the shell, we recommend you to go through this tutorial first.
If you have some experience, please check this link and make sure you are familiar with most of the concepts presented there.
Resources
Git
Git is a powerful tool used by developers all around the world to control and manage changes to a project over time.
As a version control software, it keeps track of every modification to the code in a special kind of database. If a mistake is made, developers can turn back the clock and compare earlier versions of the code to help fix the mistake while minimizing disruption to all team members.
Version control also helps teams solve compatibility problems. Changes made in one part of the software can be incompatible with those made by another developer working at the same time and this problem should be discovered and solved in an orderly manner without blocking the work of the rest of the team.
Luckily, the possibility of tracking every individual change by each contributor discourages conflicting concurrent work and, if it happens to be the case, git also offers a great variety of commands to solve this issue in a ordered way to make sure no relevant work is lost.
Please read Chapter 1.1 through 1.4 of this link. Be sure to understand The Three States section in Chapter 1.3.
Complete the tutorial
Here you got a interactive webpage that teaches you Git basic commands using visual tree representation: Learn Git Branching
You will see the page has two tabs: Main and Remote. Each tab also has sections and levels with problems to solve.
To continue, please complete the following levels:
-
Main tab
- Introduction Sequence
- Ramping Up
- Moving Work Around
- A Mixed Bag - levels 1 & 2
- Advanced Topics - only first level (Rebasing)
-
Remote tab
- Push & Pull -- Git Remotes!
- To Origin And Beyond -- Advanced Git Remotes! - levels 1, 2 & 4
Pay special attention to the rebase command and everything that relates to it (specially the advanced topic, first level). We will use it on a daily basis.
After completing all the levels previously mentioned, we highly recommend you to read the following article about writing meaningful commit messages: How to Write Better Git Commit Messages
Learn HTML
HTML is the standard markup language for creating Web pages.
- HTML stands for Hyper Text Markup Language
- HTML describes the structure of Web pages using markup
- HTML elements are the building blocks of HTML pages
- HTML elements are represented by tags
- HTML tags label pieces of content such as "heading", "paragraph", "table", and so on
- Browsers do not display the HTML tags, but use them to render the content of the page
An HTML page is just a plain text file with .html extension that you can edit on any text editor.
Please be careful and use the correct tag for each case. If you want a paragraph use <p>, if you need to add a table use <table>, for every component that you want to add to the UI, search for the correct tag. Here you can see a list of all html tags.
We use the following style guide for HTML code, except for the indentation part where we use this syntax:
<html>
<head></head>
<body>
<h1>Hello</h1>
<div>...</div>
</body>
</html>
Exercise
In the next 3 sections (HTML, CSS and JS), we are going to build a tool to create and control monthly tasks. The user can mark tasks as completed and the platform will send the user a reminder if the task hasn't been completed before end of month. At the beginning of each month the tasks will automatically get back to uncompleted.
We will start using GIT from now on, so please create a folder inside your repository each time you start a new section. You should end up with a folder structure similar to this:
html/excercise1.html
...
html/excercise3.html
css/plain_css
...
css/bootstrap
...
For this section, please build the following 3 web pages using only HTML (without CSS or Javascript). In order to check that the HTML you generated is valid, you can use this validator.
Important Note
When you create the first Pull Request for this repo, you will see that some strange files will appear in your git stage (for example .DS_Store files). These files should not be commited and can be ignored by having a .gitignore file that basically ignores whatever you tell git to ignore. You can take a look at this repo to see specific gitignore files for every language/technology.
For this section you could use a general macOS gitignore.
Learn CSS
What is CSS?
CSS is the language we use to style a Web page.
- CSS stands for Cascading Style Sheets
- CSS describes how HTML elements are to be displayed on screen, paper, or in other media
- CSS saves a lot of work. It can control the layout of multiple web pages all at once
- External stylesheets are stored in CSS files
Feel free to check this CSS guide that goes more in depth at any time.
Syntax
CSS consist of a set of rules that are interpreted by the browser and are applied to corresponding elements in the web page. Each rule is made of three parts:
-
Selector: An HTML tag at which a style will be applied. This could be any tag like
<h1>,<table>, etc. -
Property: Type of attribute of HTML tag. They could be color, border etc.
-
Value: Values are assigned to properties. For example, color property can have value either
red,#F1F1F1, etc.
Rule:
selector {
property: value;
}
You can have multiple types of selectors, below is a quick list.
Universal selector
Matches any element type.
* {
color: #000000;
}
So in this case every element is going to be black.
Type selector
Selects an element of a specific type: h1, p, table, etc.
h2 {
color: #0000FF;
}
Descendant selector
ul em {
color: #000000;
}
Applies the color to <em> elements only inside a <ul> tag.
Class selector
You can define style rules based on the class attribute of the elements. All the elements having that class will be formatted according to the defined rule.
.black {
color: #000000;
}
This rule renders the content black for every element with class attribute set to black in our document.
You can even make it more particular, only rendering the content in black for <h1> elements with a black class.
h1.black {
color: #000000;
}
You can then use this class and apply it to an HTML element like this:
<p class="black center">
This paragraph will be styled by the classes black and center.
</p>
Id selector
You can define rules based on the id attribute of the elements. All the elements having that id will be formatted according to the defined rule. You should not have more than one element with the same id in the same page.
#login-button {
color: #FF0000;
}
The true power of id selectors is when they are used as the foundation for descendant selectors. For example:
#login-form h2 {
color: #0000FF;
}
Lets us only apply the blue color to the h2 if it's a descendant of the login form (which should be unique).
Child selector
This rule is very similar to the descendant selector, but applies only to direct childs.
body > p {
color: #000000;
}
Will render all the paragraphs in black if they are direct child of <body> element.
Attribute Selectors
You can apply styles to to HTML elements with particular attributes like this:
input[type = "text"] {
color: #000000;
}
This will match all input elements with type of text (and not submit for example).
Grouping selectors
You can apply a style to many selectors just by separating the selectors by a comma like this:
h1, h2, h3 {
color: #36C;
font-weight: normal;
text-transform: lowercase;
}
Let's practice
In order to practice CSS selectors, let's complete this game here.
Layout
When you use CSS to create a layout, you are moving the elements away from the normal flow. The methods that can change how elements are laid out in CSS are as follows:
- The display property — Standard values such as block, inline or inline-block can change how elements behave in normal flow.
- Floats — Applying a float value such as left can cause block level elements to wrap alongside one side of an element, like the way images sometimes have text floating around them in magazine layouts.
- The position property — Allows you to precisely control the placement of boxes inside other boxes. Static positioning is the default in normal flow, but you can cause elements to be laid out differently using other values, for example always fixed to the top left of the browser viewport.
- The Flexbox Layout - aims at providing a more efficient way to lay out, align and distribute space among items in a container, even when their size is unknown and/or dynamic.
The display property
The main methods of achieving page layout in CSS are all values of the display property.
For example, the reason that paragraphs in English display one below the other is due to the fact that they are styled with display: block. If an HTML element has display: block, it tries to fill all the available space horizontally.
If you create a link around some text inside a paragraph, that link remains inline with the rest of the text, and doesn’t break onto a new line. This is because the <a> element is display: inline by default. display: inline sets the height and width of an element to the minimum possible and you can't change the width or height properties of the HTML element. If you want to have the same properties as display: inline but also have the possibility to change width or height, you must use display: inline-block.
Finally display: none removes the element from the layout. The display property has more available options but these are the most commonly used.
Floats
Floating an element changes the behavior of that element and the block level elements that follow it in normal flow. The element is moved to the left or right and removed from normal flow, and the surrounding content floats around the floated item.
The float property has four possible values:
- left — Floats the element to the left.
- right — Floats the element to the right.
- none — Specifies no floating at all. This is the default value.
- inherit — Specifies that the value of the float property should be inherited from the element's parent element.
<h1>Simple float example</h1>
<div class="box">Float</div>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla luctus aliquam dolor, eu lacinia
lorem placerat vulputate. Duis felis orci, pulvinar id metus ut, rutrum luctus orci. Cras
porttitor imperdiet nunc, at ultricies tellus laoreet sit amet. Sed auctor cursus massa at porta.
Integer ligula ipsum, tristique sit amet orci vel, viverra egestas ligula. Curabitur vehicula
tellus neque, ac ornare ex malesuada et. In vitae convallis lacus. Aliquam erat volutpat.
Suspendisse ac imperdiet turpis. Aenean finibus sollicitudin eros pharetra congue. Duis ornare
egestas augue ut luctus. Proin blandit quam nec lacus varius commodo et a urna. Ut id ornare
felis, eget fermentum sapien.
</p>
.box {
float: left;
width: 150px;
height: 150px;
margin-right: 30px;
}
More information about the float property here.
Positioning techniques
Positioning allows you to move an element from where it would be placed when in normal flow to another location. Positioning isn’t a method for creating your main page layouts, it is more about managing and fine-tuning the position of specific items on the page.
There are however useful techniques for certain layout patterns that rely on the position property. Understanding positioning also helps in understanding normal flow, and what it is to move an item out of normal flow.
There are five types of positioning you should know about:
- Static positioning is the default that every element gets.
- Relative positioning allows you to modify an element's position on the page, moving it relative to its position in normal flow — including making it overlap other elements on the page.
- Absolute positioning moves an element completely out of the page's normal layout flow, like it is sitting on its own separate layer. From there, you can fix it in a position relative to the edges of its nearest positioned ancestor element (element without static position).
- Fixed positioning is very similar to absolute positioning, except that it fixes an element relative to the browser viewport.
Flexbox Layout
The main idea behind the flex layout is to give the container the ability to alter its items’ width/height (and order) to best fill the available space (mostly to accommodate to all kind of display devices and screen sizes). A flex container expands items to fill available free space or shrinks them to prevent overflow.
Flex container
To use flexbox you need to define a flex container. A flex container enables a flex context for all its direct children.
.container {
display: flex; /* or inline-flex */
}
Flex orientation
You can define the direction flex items are placed in the flex container. Think of flex items as primarily laying out either in horizontal rows or vertical columns. By default flex uses horizontal orientation.
.container {
flex-direction: row | row-reverse | column | column-reverse;
}
Justify content
This defines the alignment along the main axis. It helps distribute extra free space leftover when either all the flex items on a line are inflexible, or are flexible but have reached their maximum size.
.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly |
start | end | left | right... + safe | unsafe;
}
Align Items
This defines the default behavior for how flex items are laid out along the cross axis on the current line. Think of it as the justify-content version for the cross axis (perpendicular to the main-axis).
.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline
| start | end | self-start | self-end +... safe | unsafe;
}
Flex items
Flex grow
This defines the ability for a flex item to grow if necessary. It accepts a unit-less value that serves as a proportion. It dictates what amount of the available space inside the flex container the item should take up.
If all items have flex-grow set to 1, the remaining space in the container will be distributed equally to all children. If one of the children has a value of 2, the space in the container would take up twice as much space as the others (or it will try to, at least).
Let's practice flexbox
In order to practice flexbox, let's complete this game here.
Coding styles
There are multiple ways in which CSS code can be formatted. Since having as many coding styles as developers would be catastrophic, we have a way of formatting/styling CSS code.
At eagerworks we use Google's style guide. Take some time to take a look at it.
Exercise #1
Part A
To catch and address any issues early on and prevent them from compounding, please implement only the first two sections of the proposed UI. In this excercise we will use plain CSS (you will define all the styles/classes).
You can check colors, fonts and assets on Figma. Ask your mentor for some guidance into how to check font sizes, colors, etc.
Part B
Reimplement the previous part, but now using utility classes.
Part C
Add SASS to the previous part. You can use this command to recompile the CSS on every change:
sass --watch input.scss output.css
Once you have that in place, go ahead and implement the full UI.
Part D
Now it's time to make sure that the UI is responsive and looks good on any screen size. You will need to add media queries to your CSS.
Exercise #2
Tailwind is a CSS framework that makes web implementation a lot easier. It provides numerous predefined classes and utilities, allowing us to implement any UI quickly and in a maintainable way.
Please take some time to read a Tailwind's documentation (taking special care to the breakpoints section) and then reimplement the same UI as you did on the previous exercise, but now using Tailwind. If you need to override something, try customizing the theme.
Learn JS
JavaScript ("JS" for short) is a full-fledged dynamic programming language that, when applied to an HTML document, can provide dynamic interactivity on websites.
On Google Chrome you can access a JS console by doing Right-click -> Inspect -> Console
Callbacks
A callback is a function that is to be executed after another function has finished executing — hence the name ‘call back’. JavaScript is an event driven language. This means that instead of waiting for a response before moving on, JavaScript will keep executing while listening for other events.
What if function contains some sort of code that can’t be executed immediately? For example, an API request where we have to send the request then wait for a response? To simulate this action, were going to use setTimeout which is a JavaScript function that calls a function after a set amount of time. We’ll delay our function for 500 milliseconds to simulate an API request. Our code will look like this:
function first() {
// Simulate a code delay
setTimeout(function() {
console.log(1);
}, 500);
}
function second() {
console.log(2);
}
first();
second();
So what happens now when we invoke our functions?
first();
second();
// 2
// 1
Selectors
To be able to change the UI and react to user events, we need to select HTML elements and modify them. For example we can get the title of a site by doing:
var myHeading = document.querySelector('h1');
querySelector uses CSS selectors and is the most versatile of the JS selectors. JS also provides getElementById, getElementsByClassName and others.
Events
Real interactivity on a website needs events. These are code structures which listen for things happening in the browser, running code in response. The most obvious example is the click event, which is fired by the browser when you click on something with your mouse.
document.querySelector('html').addEventListener('click', function() {
alert('Ouch! Stop poking me!');
})
Exercise 1
We will implement a wishlist. Please implement the UI showed in the images below. The user is able to see a list of all the items she/he wants to buy in the future. By pressing the "ADD" button the user will have a form to add new items to the list.
The user can also optimize which items to buy with a limited budget. When the user presses the "OPTIMIZE" button, the whishlist should be sorted from lowest cost to largest and the greatest number of items should be checked based on the budget.
Restriction: You can only use up to two iterations to optimize the wishlist.
Please use only plain JS (no libraries) in this exercise.
Exercise 2
We don't want to lose all items when the page is refreshed. To prevent this, when the user creates a new item, we need to persist it using localStorage. Then, we will populate the wishlist with the previously stored items.
Additional Resources
- How to correctly name variables
- Things you SHOULDN'T do
- If you want to go deep into Javascript, check this javascript tutorial
Learn Web Architecture
The above diagram is a representation of a simple web architecture. Next we will walk you through each component, providing an introduction to each one that should give you a good mental model for thinking through web architecture going forward.
The web follows a client/server architecture in which the server hosts, delivers and manages most of the resources and services to be consumed by the client.
The main thing to understand in a web application, is that there are basically two programs running at the same time:
- The code that lives on the server and responds to HTTP requests.
- The code that lives in the browser and responds to user input.
Client Side
The browser is a program that understands HTML, CSS, and Javascript which can also make HTTP requests. It uses those HTTP request to communicate to the Web Server. Each time the user enters a url or clicks a link the browser fires a request to the server. The web server will respond with an HTML file that the browser is capable of understanding. The client side ONLY communicates with the server with requests, it has no access to the server's code.
DNS
DNS stands for “Domain Name System” and it’s a backbone technology that makes the world wide web possible. At the most basic level, DNS provides a key/value lookup from a domain name (e.g., google.com) to an IP address (e.g., 85.129.83.120), which is required in order for your computer to route a request to the appropriate server. Analogizing to phone numbers, the difference between a domain name and IP address is the difference between “call John Doe” and “call 201-867–5309.” Just like you needed a phone book to look up John's number in the old days, you need DNS to look up the IP address for a domain. So you can think of DNS as the phone book for the internet.
Web Application Server
On the hardware side, a web server is a computer that stores web server software and a website's component files (e.g. HTML documents, images, CSS stylesheets, and JavaScript files). It is connected to the Internet and supports physical data interchange with other devices connected to the web. On the software side, a web server executes the core business logic that handles a user's request and sends back HTML to the user's browser.
Database Server
Every modern web application leverages one or more databases to store information. Databases provide ways of defining your data structures, inserting new data, finding existing data, updating or deleting existing data, performing computations across the data, and more.
REST/CRUD
What is an API?
In the context of a web platform, an API (Application Programming Interface) is an interface between the different parts of the platform. It is the way the different parts of the system communicate with each other. For example the backend could expose an API that the frontend consumes in order to retrieve information to display in a web page.
HTTP Verbs
The HTTP protocol defines methods (usually called verbs) to indicate the desired action to be performed on a specific resource. The most commonly used HTTP verbs are:
- GET: requests a representation of the specified resource. Requests using GET should only retrieve data and should have no other effect.
- POST: requests that a web server accepts the data enclosed in the body of the request message, most likely for storing it. It is often used when uploading a file or when submitting a completed web form.
- PUT: replaces the resource at the current URL with the resource contained within the request. PUT is used to both create and update the state of a resource on the server
- PATCH: applies partial modifications to a resource. It is mostly used to update a resource.
- DELETE: deletes the specified resource
- OPTIONS: returns the HTTP methods that the server supports for the specified URL. This can be used to check the functionality of a web server
- HEAD: asks for a response identical to that of a GET request, but without the response body. This is useful for retrieving meta-information written in response headers, without having to transport the entire content.
REST
REST (REpresentational State Transfer) is about constraining the way we interact between client and server, to take advantage of what the protocol (in this case, HTTP) offers. These constraints give us freedom to focus on our API design:
- Uniform interface: requests from different clients look the same, whether the client is a browser, mobile device, or anything else.
- Client-server separation: the client and the server act independently and the interaction between them is only in the form of requests and responses.
- Stateless: the server does not remember anything about the user who uses the API, so all necessary information to process the request must be provided by the client on each request. Note: this isn't about storing server-side state.
- Layered system: the client is agnostic as to how many layers, if any, there are between the client and the actual server responding to the request. This is a key principle of HTTP, allowing for caching servers, reverse proxies, and access security layering – all transparent to the client sending the request.
- Cacheable: the server response must contain information about whether or not the data is cacheable, allowing the client and/or intermediaries (see layered constraint, above) to cache data outside of the API server.
- Code-on-demand (optional): the client can request code from the server, usually in the form of a script, for client-side execution.
CRUD
CRUD stands for Create, Read, Update and Delete. In the context of a RESTful API, we want them to have a standarized use of HTTP verbs and each action on our API will map to a specific CRUD action in a database.
Example RESTful API endpoints
If our system handles photos and we want to have an API to expose them, this could be a list of endpoints we will have in our RESTful API:
| HTTP Verb | Path | Used for |
|---|---|---|
| GET | /photos | display a list of all photos |
| GET | /photos/new | return an HTML form for creating a new photo |
| POST | /photos | create a new photo |
| GET | /photos/:id | display a specific photo |
| GET | /photos/:id/edit | return an HTML form for editing a photo |
| PATCH/PUT | /photos/:id | update a specific photo |
| DELETE | /photos/:id | delete a specific photo |
Learn Databases
A database is an organized collection of data, generally stored and accessed electronically from a computer system.
- Create a database
CREATE DATABASE learn_to_code;
\c learn_to_code
- Create a table
CREATE TABLE users(
id serial PRIMARY KEY,
username VARCHAR (50) NOT NULL
);
- Insert data into tables
INSERT INTO users (username) VALUES('alex')"
- Get information from the database
SELECT * FROM users WHERE id=1;
Exercise
Let's create the reality of the previous exercises on a relational database. Our reality has users that want to choose favourite items. Items can be marked as bought and can be shared with other users. Users can follow other users to check the state of their favourites items. Users can also add comments to other users' favourite items.
First, we need to create an ERD diagram to represent this reality. This diagram will allow us to have a clear idea of the database structure. Then we will create this database on Postgres.
Finally, please create these four SQL queries:
- Fetch all the favourite items for a given user
- Fetch all users together with their favourite items, even if the user doesn't have any items
- Get the number of users without favourite items
- Fetch the name of the users that have more than five followers and some non bought favourite items
Ruby on Rails
In this section you are going to learn about the Ruby on Rails framework.
Ruby
Inspired from ruby quickstart.
Ruby comes with a program that will show the results of any Ruby statements you feed it. Playing with Ruby code in interactive sessions like this is an excellent way to learn the language. If you’re using macOS open up Terminal and type irb (Interactive Ruby), then hit enter.
Type this:
irb(main):002:0> puts "Hello World"
Hello World
=> nil
puts is the basic command to print something out in Ruby. But then what’s the => nil bit? That’s the result of the expression. putsalways returns nil, which is Ruby’s absolutely-positively-nothing value.
Methods
What if we want to say Hello a lot without getting our fingers all tired? We need to define a method!
def hi
puts "Hello World!"
end
The code def hi starts the definition of the method. It tells Ruby that we’re defining a method, that its name is hi. The next line is the body of the method, the same line we saw earlier: puts "Hello World". Finally, the last line end tells Ruby we’re done defining the method.
irb(main):013:0> hi
Hello World!
=> nil
irb(main):014:0> hi()
Hello World!
=> nil
Well, that was easy. Calling a method in Ruby is as easy as just mentioning its name to Ruby. If the method doesn’t take parameters that’s all you need. You can add empty parentheses if you’d like, but they’re not needed.
What if we want to say hello to one person, and not the whole world? Just redefine hi to take a name as a parameter.
def hi(name)
puts "Hello #{name}!"
end
irb(main):018:0> hi("Matz")
Hello Matz!
What’s the #{name} bit? That’s Ruby’s way of inserting something into a string. The bit between the braces is turned into a string (if it isn’t one already) and then substituted into the outer string at that point.
Object oriented programming
If you haven't seen object oriented programming before, we recommend you to check this before continuing. If you still want to know more about object oriented programming in Ruby, we recommend checking this course.
Classes
What if we want a real greeter around, one that remembers your name and welcomes you and treats you always with respect. You might want to use an object for that. Let’s create a Greeter class.
class Greeter
def initialize(name = "World")
@name = name
end
def say_hi
puts "Hi #{@name}!"
end
def say_bye
puts "Bye #{@name}, come back soon."
end
end
The new keyword here is class. This defines a new class called Greeter and a bunch of methods for that class. Also notice @name. This is an instance variable, and is available to all the methods of the class. As you can see it's used by say_hi and say_bye.
Now let’s create a greeter object and use it:
irb(main):035:0> greeter = Greeter.new("Pat")
=> #<Greeter:0x16cac @name="Pat">
irb(main):036:0> greeter.say_hi
Hi Pat!
=> nil
irb(main):037:0> greeter.say_bye
Bye Pat, come back soon.
=> nil
Once the greeter object is created, it remembers that the name is Pat. Hmm, what if we want to get at the name directly?
irb(main):038:0> greeter.@name
SyntaxError: (irb):38: syntax error, unexpected tIVAR, expecting '('
Nope, can’t do it.
Instance variables are hidden away inside the object. They’re not terribly hidden, you see them whenever you inspect the object, and there are other ways of accessing them, but Ruby uses the good object-oriented approach of keeping data sort-of hidden away.
So what methods do exist for Greeter objects?
irb(main):039:0> Greeter.instance_methods
=> [:say_hi, :say_bye, :instance_of?, :public_send,
:instance_variable_get, :instance_variable_set,
:instance_variable_defined?, :remove_instance_variable,
:private_methods, :kind_of?, :instance_variables, :tap,
:is_a?, :extend, :define_singleton_method, :to_enum,
:enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?,
:freeze, :inspect, :display, :send, :object_id, :to_s,
:method, :public_method, :singleton_method, :nil?, :hash,
:class, :singleton_class, :clone, :dup, :itself, :taint,
:tainted?, :untaint, :untrust, :trust, :untrusted?, :methods,
:protected_methods, :frozen?, :public_methods, :singleton_methods,
:!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]
Whoa. That’s a lot of methods. We only defined two methods. What’s going on here? Well this is all of the methods for Greeter objects, a complete list, including ones defined by ancestor classes. If we want to just list methods defined for Greeter we can tell it to not include ancestors by passing it the parameter false, meaning we don’t want methods defined by ancestors.
irb(main):040:0> Greeter.instance_methods(false)
=> [:say_hi, :say_bye]
But what if you want to be able to view or change the name? Ruby provides an easy way of providing access to an object’s variables.
irb(main):044:0> class Greeter
irb(main):045:1> attr_accessor :name
irb(main):046:1> end
=> nil
In Ruby, you can open a class up again and modify it. The changes will be present in any new objects you create and even available in existing objects of that class. So, let’s create a new object and play with its @name property.
irb(main):047:0> greeter = Greeter.new("Andy")
=> #<Greeter:0x3c9b0 @name="Andy">
irb(main):048:0> greeter.respond_to?("name")
=> true
irb(main):049:0> greeter.respond_to?("name=")
=> true
irb(main):050:0> greeter.say_hi
Hi Andy!
=> nil
irb(main):051:0> greeter.name="Betty"
=> "Betty"
irb(main):052:0> greeter
=> #<Greeter:0x3c9b0 @name="Betty">
irb(main):053:0> greeter.name
=> "Betty"
irb(main):054:0> greeter.say_hi
Hi Betty!
=> nil
Using attr_accessor defined two new methods for us, name to get the value, and name= to set it.
Flow control
This greeter isn't all that interesting though, it can only deal with one person at a time. What if we had some kind of MegaGreeter that could either greet the world, one person, or a whole list of people?
Let’s write this one in a file instead of directly in the interactive Ruby interpreter IRB.
#!/usr/bin/env ruby
class MegaGreeter
attr_accessor :names
# Create the object
def initialize(names = "World")
@names = names
end
# Say hi to everybody
def say_hi
if @names.nil?
puts "..."
elsif @names.respond_to?("each")
# @names is a list of some kind, iterate!
@names.each do |name|
puts "Hello #{name}!"
end
else
puts "Hello #{@names}!"
end
end
# Say bye to everybody
def say_bye
if @names.nil?
puts "..."
elsif @names.respond_to?("join")
# Join the list elements with commas
puts "Goodbye #{@names.join(", ")}. Come back soon!"
else
puts "Goodbye #{@names}. Come back soon!"
end
end
end
mg = MegaGreeter.new
mg.say_hi
mg.say_bye
# Change name to be "Zeke"
mg.names = "Zeke"
mg.say_hi
mg.say_bye
# Change the name to an array of names
mg.names = ["Albert", "Brenda", "Charles",
"Dave", "Engelbert"]
mg.say_hi
mg.say_bye
# Change to nil
mg.names = nil
mg.say_hi
mg.say_bye
Save this file as mega_greeter.rb, and run it as ruby mega_greeter.rb. The output should be:
Hello World!
Goodbye World. Come back soon!
Hello Zeke!
Goodbye Zeke. Come back soon!
Hello Albert!
Hello Brenda!
Hello Charles!
Hello Dave!
Hello Engelbert!
Goodbye Albert, Brenda, Charles, Dave, Engelbert. Come
back soon!
...
...
So, looking deeper at our new program, notice the initial lines, which begin with a hash mark (#). In Ruby, anything on a line after a hash mark is a comment and is ignored by the interpreter. The first line of the file is a special case, and under a Unix-like operating system (Linux, macOS, etc.) tells the shell how to run the file. The rest of the comments are there just for clarity.
Our say_hi method has become a bit trickier:
# Say hi to everybody
def say_hi
if @names.nil?
puts "..."
elsif @names.respond_to?("each")
# @names is a list of some kind, iterate!
@names.each do |name|
puts "Hello #{name}!"
end
else
puts "Hello #{@names}!"
end
end
It now looks at the @names instance variable to make decisions. If it's nil, it just prints out three dots. No point greeting nobody, right?
Cycling and Looping
If the @names object responds to each, it is something that you can iterate over, so iterate over it and greet each person in turn. Finally, if @names is anything else, just let it get turned into a string automatically and do the default greeting.
Let's look at that iterator in more depth:
@names.each do |name|
puts "Hello #{name}!"
end
each is a method that accepts a block of code then runs that block of code for every element in a list, and the bit between do and end is just such a block. A block is like an anonymous function. The variable between pipe characters (|) is the parameter for this block.
What happens here is that for every entry in a list, name is bound to that list element, and then the expression puts "Hello #{name}!" is run with that name.
Most other programming languages handle going over a list using the for loop, which in C looks something like:
for (i = 0; i < number_of_elements; i++)
{
do_something_with(element[i]);
}
This works, but isn’t very elegant. You need a throw-away variable like i, have to figure out how long the list is, and have to explain how to walk over the list. The Ruby way is much more elegant, all the housekeeping details are hidden within the each method, all you need to do is to tell it what to do with each element. Internally, the each method will essentially call yield "Albert", then yield "Brenda" and then yield "Charles", and so on.
Ruby Style Guide
Writing high quality code is essential to have easy to read and easy to maintain products. Please check the ruby styleguide.
Additional resources
- The odin project ruby course
Micro web server
A Web server is a program that uses HTTP (Hypertext Transfer Protocol) to serve web pages to users. In this step we are going to use a microframework to build a simple web server to serve our wishlist platform. A microframework is a term used to refer to minimalistic web application frameworks. It is contrasted with full-stack frameworks.
Sinatra
Sinatra is a DSL (Domain Specific Language) for quickly creating web applications in Ruby with minimal effort.
Create a server.rb file with the following content:
require 'sinatra'
get '/' do
'Hello world!'
end
In order to use the sinatra library, you will first need to install it:
gem install sinatra
A gem is a module/library that you can install and use in every project that you need.
Now you can run your server by doing:
ruby server.rb
If you navigate to localhost:4567 on your browser you will see the response from the server to your request.
Let’s add an HTML view. Put the following code into an index.erb file inside the views directory (you will need to create it):
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>
<body>
<h1><%= @message %></h1>
</body>
</html>
change your endpoint code to:
get '/' do
@title = 'Hello World!'
erb :index
end
All instance variables will be available in the view for you to use. We are using the erb extension on the view file. ERB (Embedded RuBy) is a feature of Ruby that enables you to use Ruby to generate text, in our case HTML text.
Exercise 1
Let's use Sinatra to serve the wishlist items platform we created in the Javascript section. Convert all your .html files to .erb and create endpoints to return these views and handle all existing functionality.
Use global variables to store the created favourite items and the max budget. Use a class to model the wishlist item.
NOTE:
If you want to show assets (i.e. images) on your views, you will need to create a public folder and store your assets there.
Exercise 2
Now, use a database to store the created favourite items. Use the pg gem as the interface to interact with a PostgreSQL database.
Exercise 3
Let's add a search input (and endpoint) that filters the wishlist items based on their name.
Ruby on Rails
Ruby on Rails is a web application development framework written in the Ruby programming language. Please read the rails getting started to learn about the framework.
Intro exercise: Hotwire
DHH (Rails creator) recently introduced the Hotwire framework, which is an alternative approach to building modern web applications without using much JavaScript. It has three parts: Turbo, Stimulus, and Hotwire Native.
Turbo and Stimulus are now deeply intergrated into modern Rails versions. Follow this tutorial to know more about Rails and Hotwire.
The Odin project has some good reads, like A Railsy Web Refresher, so we recommend to investigate those resources too.
Exercise
We will now implement a complete platform for companies to organize events. Please check the platforms requirements and their estimates.
Create an account on Trello and add all the user stories to a board. We will use Trello to keep track of the progress and communicate any possible question to the client. In this exercise the client is eagerworks.
In the Trello board we will have 5 columns: Backlog, Current Sprint, In Progress, To Check, Done.
The Backlog has all the platform user stories. The Current Sprint column has the user stories selected for this sprint iteration. Every time we start developing a new user story we need to move that user story to the In Progress column. We move to the To Check column when the user story is ready to be tested by the user. If the client validates the user story she/he moves the card to the Done column.
You can check the UI on Figma.
How to test your Rails app
RSpec
RSpec is a testing tool for Ruby, created for behavior-driven development (BDD). It is the most frequently used testing library for Ruby in production applications. Even though it has a very rich and powerful DSL (domain-specific language), at its core it is a simple tool which you can start using rather quickly.
Apart from the Rspec gem we will use the following tools:
Database Cleaner
Before each test case we need to make sure that our database is clean. If we leave information on the database, this information can cause unwanted failures.
FactoryBot
To test our platform we need data. There are two ways to create data for our tests without creating everything manually: Fixtures and Factories (Please look up the difference). At eagerworks we use factories to create our test data.
Faker
We have factories but adding emails, names, addresses to our factories can be a pain in the ass. That is why we use faker gem to make it easier.
Shoulda Matchers
Shoulda Matchers provides one-liners to test common Rails functionality. We will use Shoulda Matchers to test model associations and validations.
Capybara
Capybara helps us test web applications by simulating how a real user would interact with your app. This tool will provide a test server for us to use on our tests.
Selenium
By default, Capybara uses the :rack_test driver, which is fast but limited: it does not support JavaScript, nor it is able to access HTTP resources outside of your Rack application, such as remote APIs and OAuth services. To get around these limitations we will use Selenium driver.
Simplecov
SimpleCov is a code coverage analysis tool for Ruby. We will use this tool to make sure that most of our code is being tested.
Step by Step
1) Create your factories
To start testing our Rails app we need data. We need to create factories for each model that we have on our Rails project.
spec/factories/user.rb
FactoryBot.define do
factory :user do
client
name { Faker::Name.name }
email { Faker::Internet.email }
phone_number { '099999999' }
password { Faker::Internet.password }
validated { true }
end
end
2) Test your models
All the logic of our Rails app uses the defined models. We need to make sure that our models work as expected before testing anything else. When testing a model it's very important to check that the factory that we created for that model is valid. The next step is to test the model validations and associations. Finally we need to implement unit tests for each of the model's method.
require 'rails_helper'
RSpec.describe User, type: :model do
it 'has a valid factory' do
expect(build(:user)).to be_valid
end
describe 'associations' do
it { is_expected.to have_many :purchases }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:first_name) }
it { is_expected.to validate_presence_of(:last_name) }
it { is_expected.to validate_presence_of(:phone_number) }
it { is_expected.to validate_presence_of(:email) }
end
describe '#method' do
end
end
3) Test your services
Implement unit tests for your services.
4) Feature tests
Feature specs are high-level tests meant to exercise slices of functionality through an application. They should drive the application only via its external interface, usually web pages.
Feature specs don't have to pay too much attention to HTML structure. They need to validate that your app features are working as expected.
For example, let's test the login feature:
require 'rails_helper'
RSpec.feature 'user login', type: :feature do
let!(:user) { create(:user, password: 'specspec') }
context 'with valid credentials' do
it 'redirects logged in user to categories index' do
visit '/users/sign_in'
fill_in 'user_email', with: user.email
fill_in 'user_password', with: user.password
click_button 'Log in'
expect(page).to have_current_path(categories_path)
end
end
context 'with invalid credentials' do
xit 'shows login error'
end
end
end
Node
In this section you are going to learn about the Node.js framework and React.
Introduction
In the following sections of the onboarding, we will cover multiple technologies. We will also provide some useful resources to help you learn about them. It's normal to feel overwhelmed at first by the number of technologies and tools we will be using, but the idea here is for you to grasp the main concepts of each one.
Node.js
Node.js is an open-source JavaScript runtime environment that runs on the V8 engine (V8 is the JavaScript execution engine that was initially built for Google Chrome. Written in C++, V8 compiles JavaScript source code to native machine code). We use the V8 engine to execute JavaScript code outside a web browser. Node.js represents a "JavaScript everywhere" paradigm, unifying web-application development around a single programming language, rather than having different languages for the frontend (client-side) and the backend (server-side). Node.js operates on a single-thread event loop, using non-blocking I/O calls, allowing it to support tens of thousands of concurrent connections without incurring in the cost of thread context switching.
Resources:
TypeScript
JavaScript, one of the world's most-used programming languages, has become the official language of the web. Developers use it to write cross-platform applications that can run on any platform and in any browser.
Although JavaScript is used to create cross-platform apps, it wasn't conceived for large apps involving thousands or even millions of lines of code. JavaScript lacks the features of more mature languages that power today's sophisticated applications. Integrated development editors (IDEs) can find it challenging to manage JavaScript and maintain these large code bases.
TypeScript addresses the limitations of JavaScript, doing so without compromising the critical value proposition of JavaScript: the ability to run your code anywhere and on every platform, browser, or host.
TypeScript is an open-source language that was developed by Microsoft. It is a superset of JavaScript, which means you can continue using the JavaScript skills you've already acquired and add specific features previously unavailable to you.
Typescript works by adding types on top of JavaScript. Types are a way to describe the shape of an object, providing better documentation and allowing TypeScript to validate that your code is working correctly.
It does this by providing extra syntax, which is compiled into JavaScript.
Why TypeScript?
The core feature of TypeScript is its type system. In TypeScript, you can identify the data type of a variable or parameter by using a type hint.
Through static type checking, TypeScript catches code issues early in development that JavaScript can't usually catch until the code is run in the javascript engine. Types also let you describe what your code is intended to do. If you're working on a team, a teammate who comes in after you can easily understand it too.
Types also power the intelligence and productivity benefits of development tools like IntelliSense. This is significantly potentiated by using inference.
Inference is the process of determining the type of a variable based on its usage. For example, if you declare a variable and initialize it with a string, TypeScript will infer that it is a string. So most of the time, you will write almost JavaScript code, but with the added benefit of type checking.
Gotchas
It is important to note that we never run TypeScript. We are always running JS code. TypeScript is compiled (or transpiled) into JS code since browsers (and node) do not understand TypeScript.
This means that types are not checked at runtime, only at compile time. So we must be careful with unexpected cases like APIs or user input where we can't be sure of the type.
Examples
These are some small examples of typescript. We recommend you to try them in the Typescript Playground.
Basic Types
// Boolean
let isDone: boolean = false;
// In this case, we can let typescript infer the type from the assingment. The following syntax is equivalent:
let isDoneInferred = false;
// Declaring a type
type SomeType = {
example: string;
};
let example: SomeType = { example: 'example' };
// This will produce a compile error
let badExample: SomeType = { example: 123 };
// Interface are types that can be extended
interface SomeInterface {
example: string;
}
interface SomeInterface2 extends SomeInterface {
example2: string;
}
let example2: SomeInterface2 = { example: 'example', example2: 'example2' };
// Function
function add(a: number, b: number): number {
return a + b;
}
// The return type is optional since it can be inferred
function addInferred(a: number, b: number) {
return a + b;
}
// Arrow function
const addArrow = (a: number, b: number): number => {
return a + b;
};
// The return type is optional since it can be inferred
const addArrowInferred = (a: number, b: number) => {
return a + b;
};
Examples in React
interface Props {
name: string;
}
// Here the return type is inferred to be a ReactElement
const Example = ({ name }: Props) => {
return <div>{name}</div>;
};
// Same here
function Example({ name }: Props) {
return <div>{name}</div>;
}
// If we want it to be explicit
const Example: React.FC<Props> = ({ name }) => {
return <div>{name}</div>;
};
// or
function Example({ name }: Props): ReactElement {
return <div>{name}</div>;
}
Resources
There are many more details of typescript. The ones specified above are only the basics. You can find more information in the following resources:
- Typescript in 100 seconds
- Typescript - The Basics
- Microsoft Typescript Course
- Beginner's TypeScript by Matt Pocock
- Typescript Tips by Matt Pocock
- Typescript: The Big Picture
- TypeScript: Getting Started
REST API
When an application starts to grow, it's common practice to split the frontend (client part) from the backend (server part). If the application is big enough, this usually helps with maintainability.
In this section we are going to create a REST API to manage our wishlist items. We will use Hono as our web framework and Prisma to interact with the database.
Bun
Throughout this guide we will use Bun as our JavaScript runtime and package manager instead of Node.js + npm. Bun is a fast, all-in-one toolkit that includes a runtime, bundler, test runner, and package manager. It is fully compatible with Node.js APIs and npm packages, but significantly faster at installing dependencies and running scripts.
Install it with:
curl -fsSL https://bun.sh/install | bash
After installation, verify it works:
bun --version
From here on, every command in this guide uses bun (instead of npm) and bunx (instead of npx).
Resources
Hono
Hono (Japanese for "flame") is an ultrafast, lightweight web framework built on Web Standards (the Request/Response API). It has first-class TypeScript support, zero dependencies, and runs on virtually any JavaScript runtime: Node.js, Deno, Bun, Cloudflare Workers, and more.
If you've used Express before, Hono will feel familiar but more modern. The key differences are:
- Built-in JSON parsing — no need for a body-parser middleware, just call
await c.req.json(). - Sub-apps via
app.route()— instead ofexpress.Router(), you create anew Hono()and mount it on a path. - Context object (
c) — all request/response operations go through the context:c.req.param(),c.req.query(),c.json(), etc. - Validation — built-in integration with Zod via
@hono/zod-validatorfor end-to-end type-safe request validation.
Quick example:
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
serve({ fetch: app.fetch, port: 3000 })
Resources
Prisma
An ORM (Object-Relational Mapping) is a library that lets you interact with a database using your programming language instead of writing raw SQL. If you're unfamiliar with the concept, watch this Introduction to ORMs video first.
Prisma is a next-generation TypeScript ORM that takes a schema-first approach. Instead of defining your database schema in TypeScript code, you declare your models in a dedicated schema.prisma file, and Prisma generates a fully type-safe client tailored to your schema. This means you get autocompletion for every model, field, and relation — and type errors at compile time if your queries don't match your database.
Key characteristics:
- Prisma Schema Language (PSL) — you define your models, fields, and relations in a
.prismafile. This serves as the single source of truth for your database structure and the generated client. - Prisma Client — an auto-generated, type-safe query builder. After any schema change, run
prisma generateand your client is updated with the new types. Queries use an intuitive, object-based API (prisma.user.findMany(),prisma.item.create(), etc.). - Prisma Migrate — a migration tool that generates SQL migration files from schema changes, tracks migration history, and applies them to your database.
- Prisma Studio — a visual database browser that lets you view and edit your data directly in the browser.
Quick example:
// schema.prisma — define your models
model Item {
id Int @id @default(autoincrement())
name String @db.VarChar(255)
purchased Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
}
// usage — query with the Prisma Client
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// SELECT * FROM items WHERE purchased = false
const wishlist = await prisma.item.findMany({ where: { purchased: false } })
// INSERT INTO items (name) VALUES ('New Item') RETURNING *
const newItem = await prisma.item.create({ data: { name: 'New Item' } })
// UPDATE items SET purchased = true WHERE id = 1
await prisma.item.update({ where: { id: 1 }, data: { purchased: true } })
// DELETE FROM items WHERE id = 1
await prisma.item.delete({ where: { id: 1 } })
Resources
- Prisma — What is Prisma?
- Getting started with PostgreSQL
- Prisma Schema reference
- CRUD operations
- Prisma Migrate
- Prisma Studio
Exercise 1 — Building the API
Using Hono and Prisma, create REST endpoints under the /api namespace to manage wishlist items. Your API should support the standard CRUD operations:
| HTTP Verb | Path | Description |
|---|---|---|
| GET | /api/items | List all wishlist items |
| POST | /api/items | Create a new item |
| GET | /api/items/:id | Get a specific item |
| PUT | /api/items/:id | Update a specific item |
| DELETE | /api/items/:id | Delete a specific item |
Steps to get started:
- Scaffold a new Hono project:
bun create hono@latest(select thenodejstemplate). - Install Prisma:
bun add @prisma/clientandbun add -D prisma. - Initialize Prisma:
bunx prisma init— this creates aprisma/schema.prismafile and a.envwith aDATABASE_URLplaceholder. - Define your models in
prisma/schema.prismaand runbunx prisma migrate devto create and apply migrations. - Create your route handlers. Use
app.route('/api/items', itemRoutes)to keep things organized. - Validate request bodies with
@hono/zod-validatorand Zod schemas.
Exercise 2 — Enhancing the API
After the basic CRUD operations are implemented, let's add more features:
- Filtering — As a user, I want to be able to filter wishlist items by name. Add support for a
searchquery parameter on the list endpoint (e.g.,GET /api/items?search=book). Use Prisma'scontainsfilter withmode: 'insensitive'for case-insensitive matching. - Pagination — Add
pageandlimitquery parameters to the list endpoint. Validate them with Zod (usingz.coerce.number()since query params are strings). - Error handling — Return proper error responses (404 when an item doesn't exist, 422 for validation errors). Use Hono's
HTTPExceptionandapp.onErrorfor centralized error handling.
React
React is a JavaScript library for building user interfaces. It uses a component-based architecture where you build complex UIs from small, isolated pieces of code called "components", following a declarative approach.
React is the most widely used frontend library, with a large ecosystem and strong community. It is used by Meta, Vercel, Airbnb, and many more.
Core Concepts
JSX
JSX is a syntax extension to JavaScript that looks like HTML but has the full power of JavaScript. It makes it easier to write and read UI code in React. JSX produces React "elements" that describe what should appear on screen.
// Static element
const element = <h1>Hello, world!</h1>
// JavaScript expressions inside curly braces
const name = 'John Doe'
const greeting = <h1>Hello, {name}</h1>
// Event listeners
const Example = () => (
<button onClick={() => alert('Hello world')}>Click me</button>
)
Components
Components are the building blocks of React applications — reusable pieces of code that return React elements. Always use function components (class components are legacy and should be avoided).
const Greeting = () => {
return <div>Hello!</div>
}
// With implicit return
const Greeting = () => <div>Hello!</div>
Props
Props are how components receive data from their parents. They are passed as attributes and follow a one-way data flow — a child can read its props but never modify them.
type GreetingProps = {
name: string
}
const Greeting = ({ name }: GreetingProps) => {
return <div>Hello {name}</div>
}
const App = () => {
return <Greeting name="John" />
}
All React components must act like pure functions with respect to their props.
State
State holds data that may change over the lifetime of a component. When state is updated, React re-renders the component and its children to reflect the new data.
You declare state using the useState hook, which returns the current value and a setter function:
const Counter = () => {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
Hooks
Hooks are functions that let you use React features (state, side effects, context, etc.) from function components. The two most fundamental hooks are:
useState— declares a state variable (seen above).useEffect— runs side effects (data fetching, subscriptions, DOM manipulation) after render. It takes a callback and a dependency array that controls when it re-runs.
const Example = () => {
const [count, setCount] = useState(0)
useEffect(() => {
// Runs when `count` changes
document.title = `You clicked ${count} times`
return () => {
// Cleanup: runs before the next effect or when the component unmounts
console.log('Cleaning up')
}
}, [count])
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
React ships many more built-in hooks (useContext, useRef, useMemo, useCallback, etc.). Read about them in the React Hooks reference.
Virtual DOM
React uses a Virtual DOM (VDom) — a lightweight JavaScript representation of the actual DOM. When state changes, React builds a new VDom tree, diffs it against the previous one, and only applies the minimal set of changes to the real DOM.
You can learn more about this in this article from Mosh Hamedani.
Resources
- React — Official documentation
- React in 100 seconds
- React Hooks reference
- Thinking in React
- Beginner's TypeScript by Matt Pocock
TanStack Query
When building a React app that talks to an API, you quickly run into challenges: loading states, caching, refetching stale data, optimistic updates, etc. TanStack Query (formerly React Query) handles all of this out of the box.
Key characteristics:
- Automatic caching — query results are cached and shared across components. No need to lift state or use global stores for server data.
- Background refetching — stale data is shown instantly while fresh data is fetched in the background.
- Loading and error states — every query returns
isLoading,isError, anddata, making it trivial to handle UI states. - Mutations —
useMutationhandles create/update/delete operations with built-in support for optimistic updates and cache invalidation.
Quick example:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Fetching data
const ItemList = () => {
const { data: items, isLoading } = useQuery({
queryKey: ['items'],
queryFn: () => fetch('/api/items').then((res) => res.json()),
})
if (isLoading) return <p>Loading...</p>
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// Creating data
const AddItemForm = () => {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newItem: { name: string }) =>
fetch('/api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newItem),
}),
onSuccess: () => {
// Refetch the items list after a successful creation
queryClient.invalidateQueries({ queryKey: ['items'] })
},
})
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutation.mutate({ name: formData.get('name') as string })
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Item name" required />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Item'}
</button>
</form>
)
}
Resources
Exercise — Wishlist Frontend
After creating the REST API, let's build a React application to consume it. The app should let users view, add, edit, and delete wishlist items.
Steps to get started:
- Scaffold a new React project with Vite:
bun create vite wishlist-client --template react-ts. - Install TanStack Query:
bun add @tanstack/react-query. - Set up a
QueryClientProviderat the root of your app (see the quick start guide). - Create a component that lists all items using
useQueryto fetch fromGET /api/items. - Create a form component that adds a new item using
useMutationand invalidates the items query on success. - Add edit and delete functionality for each item.
- Handle loading and error states in all views.
Full-Stack Project
This section builds on everything from the Node + React track and takes it further. You will learn how to structure a production-grade monorepo, build a mobile app with Expo, and share code across multiple applications.
By the end of this section you will build Chirp — a Twitter clone with a web app, an API, and a mobile app, all living in a single Turborepo monorepo powered by Bun.
Chirp
Chirp is a Twitter clone built as a full-stack application with a web app, an API, and a mobile app — all living in a single monorepo. This is the capstone exercise for the Node + React track.
Requirements
Check the project requirements and their estimates: Requirements spreadsheet
UI
Check the UI designs on Figma: Figma
Core Features
At a high level, Chirp includes:
- Authentication — sign up, log in, social sign-on.
- Tweets — create, read, and delete tweets.
- Timeline — a paginated feed of tweets from users you follow.
- User profiles — view any user's profile and their tweets.
- Follow / Unfollow — follow and unfollow other users.
- Likes — like and unlike tweets.
- Search — search for users and tweets.
- Notifications & Emails — in-app notifications and transactional emails.
Apps
| App | Technology | Purpose |
|---|---|---|
apps/web | Next.js 16 | Web application — SSR, caching, Server Components |
apps/api | Hono + Prisma | API server — tRPC procedures, database access |
apps/mobile | Expo + NativeWind | Mobile application — iOS and Android |
Packages
| Package | Purpose |
|---|---|
@chirp/trpc | tRPC router and procedures — shared type-safe API layer |
@chirp/auth | Better Auth configuration — shared auth logic |
@chirp/email | React Email templates + Resend sending |
Additional packages (e.g., @chirp/db, @chirp/config) are your decision. Extract shared concerns as you see fit.
Methodology
Create an account on Linear or any other ticket management tool, and add all the user stories to a board. We will use this to keep track of the progress and communicate any possible question to the client. In this exercise the client is eagerworks.
In the board we will have 5 columns:
- Backlog — all the platform user stories.
- Current Sprint — user stories selected for this sprint iteration.
- In Progress — the user story you are currently developing.
- To Check — user stories ready to be tested by the client.
- Done — user stories validated by the client.
Every time you start developing a new user story, move it to In Progress. When it is ready for review, move it to To Check. If the client validates it, they move it to Done.
Monorepo
A monorepo is a single repository that contains multiple projects (apps, packages, libraries) that can depend on each other. Instead of scattering code across many repositories and publishing packages to a registry, everything lives together and shares tooling, configuration, and types.
Why it matters:
- Shared code — extract common logic into packages that multiple apps import directly, with no publish step.
- Atomic changes — a single PR can update the API, the web app, and the mobile app together.
- Consistent tooling — one ESLint config, one TypeScript config, one CI pipeline.
Turborepo
Turborepo is a build system for JavaScript and TypeScript monorepos. It orchestrates tasks (build, lint, test) across all packages, understands the dependency graph between them, and caches results so unchanged packages are never rebuilt.
Key concepts:
turbo.json— defines your task pipeline: which tasks exist, what they depend on, and what outputs to cache.- Task dependencies —
"dependsOn": ["^build"]means "build my dependencies before building me". The^prefix means "dependencies in other packages". - Caching — Turborepo hashes your source files and configuration. If nothing changed, it replays the cached output instead of re-running the task.
- Filtering — run tasks for a specific package with
turbo run build --filter=@chirp/web.
Bun Workspaces
Bun supports workspaces natively. You declare them in the root package.json:
{
"name": "chirp",
"workspaces": ["apps/*", "packages/*"]
}
Then packages can reference each other using the workspace:* protocol:
{
"name": "@chirp/web",
"dependencies": {
"@chirp/trpc": "workspace:*",
"@chirp/auth": "workspace:*"
}
}
When you run bun install at the root, Bun links workspace packages together so imports resolve instantly — no publishing or version bumping needed.
Resources
- Turborepo — Documentation
- Turborepo — Structuring a repository
- Turborepo — Configuring tasks
- Bun — Workspaces
Getting Started
1. Create the monorepo
bunx create-turbo@latest chirp
This gives you a working monorepo with apps/ and packages/ directories, a root turbo.json, and workspace configuration already set up.
2. Clean up the boilerplate
The scaffolded project comes with example apps and packages. Remove them — delete everything inside apps/ and packages/ so you start with empty directories. You will add each app and package yourself in the sections that follow.
3. Configure turbo.json
Define your task pipeline so turbo run dev starts all apps, turbo run build builds them in the correct order, and turbo run lint checks everything:
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"dev": {
"persistent": true,
"cache": false
},
"lint": {
"dependsOn": ["^build"]
}
}
}
Project structure
Once you complete all the sections, the monorepo will look like this:
chirp/
├── apps/
│ ├── web/ # Next.js
│ ├── api/ # Hono
│ └── mobile/ # Expo
├── packages/
│ ├── trpc/ # tRPC router + procedures
│ ├── auth/ # Better Auth configuration
│ ├── email/ # Email templates + sending
│ └── ... # Other shared packages
├── turbo.json
├── package.json
└── bun.lock
Now continue to the Next.js section to set up the web and API apps.
Next.js
Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js provides the architecture, optimizations, and tooling on top — routing, server-side rendering, bundling, and more — so you can focus on building your application instead of spending time with configuration.
We will be using Next.js 16, the latest major version. It ships with Turbopack as the default bundler (2–5x faster builds), stable React Compiler support, and a new caching model called Cache Components. These are the key concepts you should understand before building Chirp.
React Server Components (RSC)
React Server Components allow you to render components on the server. This means the server does the heavy lifting (data fetching, rendering) and sends the result to the browser, reducing bundle size and improving performance.
In Next.js, all components are Server Components by default. If a component needs interactivity (state, effects, event handlers, browser APIs), you opt it into the client by adding "use client" at the top of the file.
// This is a Server Component by default — runs on the server
export default async function PostList() {
const posts = await prisma.post.findMany()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
'use client'
// This is a Client Component — runs in the browser
import { useState } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked' : 'Like'}
</button>
)
}
The mental model: Server Components for data and layout, Client Components for interactivity. You can nest Client Components inside Server Components (but not the other way around).
Resources
React Compiler
The React Compiler is a build-time tool that automatically optimizes your React application through automatic memoization. It understands the Rules of React and applies optimizations that you would otherwise have to write manually with useMemo, useCallback, and React.memo.
Before the compiler, you had to manually memoize to prevent unnecessary re-renders:
// Before: manual memoization
import { useMemo, useCallback, memo } from 'react'
const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
const sorted = useMemo(() => expensiveSort(items), [items])
const handleSelect = useCallback((id) => onSelect(id), [onSelect])
return sorted.map((item) => (
<Item key={item.id} onClick={() => handleSelect(item.id)} />
))
})
// After: the compiler handles it automatically
function ExpensiveList({ items, onSelect }) {
const sorted = expensiveSort(items)
const handleSelect = (id) => onSelect(id)
return sorted.map((item) => (
<Item key={item.id} onClick={() => handleSelect(item.id)} />
))
}
The compiler figures out the optimal memoization boundaries at build time — you just write straightforward code.
Enable it in next.config.ts:
const nextConfig = {
reactCompiler: true,
}
export default nextConfig
Note: The React Compiler relies on Babel, so compile times will be slightly higher when enabled.
Resources
Cache Components
Previous versions of Next.js applied implicit caching — it was hard to predict what was cached and what wasn't. Next.js 16 flips this model: everything is dynamic by default, and you explicitly opt into caching with the "use cache" directive.
Cache Components work together with Partial Pre-Rendering (PPR): at build time, Next.js renders a static HTML shell from everything it can resolve ahead of time. Dynamic parts are wrapped in <Suspense> and stream in at request time.
This gives you the speed of static sites with the flexibility of dynamic rendering.
Enable Cache Components in next.config.ts:
const nextConfig = {
cacheComponents: true,
}
export default nextConfig
How it works
There are three types of content in a Cache Components page:
- Static content — plain markup, synchronous computations. Automatically included in the static shell.
- Cached dynamic content — data from APIs or databases that doesn't change often. Mark it with
"use cache"and control its lifetime withcacheLife. - Request-time dynamic content — personalized or real-time data (cookies, headers). Wrap it in
<Suspense>to stream it at request time.
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
export default function BlogPage() {
return (
<>
{/* 1. Static — automatically in the shell */}
<h1>Our Blog</h1>
{/* 2. Cached — included in the shell, revalidated hourly */}
<BlogPosts />
{/* 3. Dynamic — streams at request time */}
<Suspense fallback={<p>Loading preferences...</p>}>
<UserPreferences />
</Suspense>
</>
)
}
async function BlogPosts() {
'use cache'
cacheLife('hours')
const posts = await fetch('https://api.example.com/posts').then((r) => r.json())
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
async function UserPreferences() {
const theme = (await cookies()).get('theme')?.value || 'light'
return <p>Your theme: {theme}</p>
}
Tagging and revalidation
Use cacheTag to tag cached data, then invalidate it after mutations:
updateTag(tag)— use in Server Actions when users need to see their changes immediately (read-your-writes).revalidateTag(tag, profile)— use when eventual consistency is acceptable (stale-while-revalidate).
import { cacheTag, cacheLife } from 'next/cache'
import { updateTag } from 'next/cache'
async function getCart() {
'use cache'
cacheTag('cart')
cacheLife('max')
// fetch cart data...
}
async function addToCart(itemId: string) {
'use server'
// write to database...
updateTag('cart') // user sees the update immediately
}
Resources
- Cache Components — Getting started
use cachedirectivecacheLifeAPI referencecacheTagAPI reference- Next.js 16 blog post
Adding the Apps to the Monorepo
Now scaffold the web and API apps inside the Chirp monorepo.
Web — Next.js
cd apps
bunx create-next-app@latest web
Select TypeScript, Tailwind CSS, and the App Router when prompted. Then enable the React Compiler and Cache Components in next.config.ts:
const nextConfig = {
reactCompiler: true,
cacheComponents: true,
}
export default nextConfig
API — Hono
cd apps
bun create hono api
This is where your tRPC router will be mounted and where Prisma handles database access. You already learned about Hono and Prisma in the REST API section — the same concepts apply here.
Run bun install at the monorepo root to link everything, then verify both apps start with turbo run dev.
Now continue to the Shared Packages section to set up tRPC, auth, and email.
Shared Packages
One of the main benefits of a monorepo is extracting shared logic into internal packages. Instead of duplicating code across apps, you create a package once and import it everywhere. In the Chirp monorepo, shared packages live in the packages/ directory and are scoped under @chirp/.
tRPC
tRPC (TypeScript Remote Procedure Call) enables you to build fully type-safe APIs without a separate schema language or code generation. It ensures server and client types stay in sync, catching type errors at compile time rather than runtime.
Key things to know about tRPC:
- It is built on top of TanStack Query, so everything you learned about
useQueryanduseMutationapplies here too. When tRPC's docs aren't clear, TanStack Query's documentation often fills the gap. - It uses a procedure-based API instead of REST endpoints. You define queries and mutations as typed functions, and call them from the client with full autocompletion.
- There is a great 5-minute introduction by Matt Pocock that covers the basics.
Monorepo pattern
The tRPC router and all procedures live in packages/trpc. Both the web and mobile apps import the AppRouter type from @chirp/trpc to create fully typed clients — no code duplication.
packages/trpc — defines the router:
import { router, publicProcedure } from './trpc'
export const appRouter = router({
tweet: tweetRouter,
user: userRouter,
// ...
})
export type AppRouter = typeof appRouter
apps/api — mounts the router via @hono/trpc-server:
import { trpcServer } from '@hono/trpc-server'
import { appRouter } from '@chirp/trpc'
app.use('/trpc/*', trpcServer({ router: appRouter }))
apps/web — creates a typed React client:
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@chirp/trpc'
export const trpc = createTRPCReact<AppRouter>()
apps/mobile — same pattern, different transport if needed:
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@chirp/trpc'
export const trpc = createTRPCReact<AppRouter>()
Resources
Better Auth
Better Auth is a TypeScript-first authentication framework. It is framework-agnostic but has first-class integrations for both Next.js and Expo.
Key features:
- Email/password auth with built-in session management.
- Social sign-on — GitHub, Google, Discord, and more via OAuth providers.
- Database-backed sessions — works directly with your existing Prisma schema and PostgreSQL database, no separate auth database needed.
- Plugin system — extend with two-factor auth, organizations/teams with roles, and more.
Monorepo pattern
The auth configuration lives in packages/auth. Each app imports it and uses the appropriate framework integration.
packages/auth — shared auth configuration:
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'
import { prisma } from '@chirp/db'
export const auth = betterAuth({
database: prismaAdapter(prisma, { provider: 'postgresql' }),
emailAndPassword: { enabled: true },
// social providers, plugins, etc.
})
apps/web — uses the Next.js integration to handle cookies and sessions in Server Components and Server Actions.
apps/mobile — uses the Expo integration for native auth flows and secure token storage.
Resources
For transactional emails (welcome emails, notifications, password resets), use React Email to build templates as React components and Resend to send them.
This package is intentionally light — research the libraries, explore the docs, and decide how to structure it for your project.
Resources
Other Packages
You are encouraged to extract additional shared concerns into packages as the project grows. Some ideas:
@chirp/db— Prisma client and schema, shared across the API and any package that needs database access.@chirp/config— shared ESLint, TypeScript, and Prettier configurations.
The decision of what to extract is yours. If you find yourself duplicating code across apps, that is a signal to create a package.
Adding the Packages to the Monorepo
Create packages/trpc, packages/auth, and packages/email directories. Each package gets its own package.json with the @chirp/ scope:
{
"name": "@chirp/trpc",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts"
}
Then in each app's package.json, add the shared packages as dependencies:
{
"dependencies": {
"@chirp/trpc": "workspace:*",
"@chirp/auth": "workspace:*",
"@chirp/email": "workspace:*"
}
}
Run bun install at the root to link everything.
Now continue to the Mobile with Expo section to add the mobile app.
Mobile with Expo
Expo is a framework built on top of React Native that provides the tooling and services needed to build, deploy, and iterate on mobile apps. It is to React Native what Next.js is to React — it handles the hard parts (bundling, native module linking, OTA updates) so you can focus on building features.
React Native Basics
React Native uses native UI primitives instead of HTML elements. The core mapping:
| Web | React Native |
|---|---|
<div> | <View> |
<p>, <span> | <Text> |
<img> | <Image> |
<input> | <TextInput> |
<button> | <Pressable> |
<ul> / <li> | <FlatList> |
<a> | <Link> (Expo Router) |
All text must be wrapped in a <Text> component — you cannot place raw strings inside a <View>.
import { View, Text, Pressable } from 'react-native'
export default function Welcome() {
return (
<View>
<Text>Hello from React Native!</Text>
<Pressable onPress={() => alert('Pressed!')}>
<Text>Tap me</Text>
</Pressable>
</View>
)
}
Expo Router
Expo Router provides file-based routing for React Native, similar to the Next.js App Router. Files in the app/ directory become routes:
app/
├── _layout.tsx # Root layout (navigation structure)
├── index.tsx # Home screen (/)
├── login.tsx # Login screen (/login)
└── (tabs)/
├── _layout.tsx # Tab layout
├── feed.tsx # Feed tab (/feed)
└── profile.tsx # Profile tab (/profile)
Layouts, dynamic routes ([id].tsx), and groups ((tabs)) work the same way as in Next.js. If you understood the App Router, Expo Router will feel familiar.
Development Workflow
Expo provides several ways to run your app during development:
- Expo Go — a pre-built app on your phone. Scan a QR code and your app loads instantly. Great for quick iteration, but limited to APIs included in the Expo SDK.
- Development builds — a custom build of your app installed on a simulator or device. Supports any native module. Use this when you need libraries not available in Expo Go.
- Simulators — Xcode's iOS Simulator and Android Studio's Emulator. Run on your machine without a physical device.
Adding the Mobile App to the Monorepo
Scaffold a new Expo app inside the Chirp monorepo:
cd apps
bunx create-expo-app@latest mobile
Expo automatically detects monorepo setups — no manual Metro bundler configuration is needed.
Add the shared packages to the mobile app's package.json:
{
"dependencies": {
"@chirp/trpc": "workspace:*",
"@chirp/auth": "workspace:*"
}
}
Run bun install at the root, then verify the mobile app starts with turbo run dev --filter=@chirp/mobile.
NativeWind
NativeWind brings Tailwind CSS to React Native. Instead of using StyleSheet.create(), you write Tailwind classes just like you do on the web.
Before (plain React Native):
import { View, Text, StyleSheet } from 'react-native'
export default function Card() {
return (
<View style={styles.card}>
<Text style={styles.title}>Hello</Text>
</View>
)
}
const styles = StyleSheet.create({
card: { padding: 16, backgroundColor: '#fff', borderRadius: 8 },
title: { fontSize: 18, fontWeight: 'bold' },
})
After (NativeWind):
import { View, Text } from 'react-native'
export default function Card() {
return (
<View className="p-4 bg-white rounded-lg">
<Text className="text-lg font-bold">Hello</Text>
</View>
)
}
Same result, but with the Tailwind workflow you already know from the web.
Resources
- Expo — Documentation
- Expo Router — Introduction
- React Native — Core Components
- NativeWind — Documentation
AI
In this section you are going to learn about the AI techniques we use at eagerworks.
Please clone and follow this Trello board (Options -> Copy Board).
Career development
In the following sections, you'll learn about the must-have knowledge to become a senior developer.
Common
One of the things that really speaks about someone's experience is how they are able to make good "business" decisions. Here are some examples:
- Do we need to delete users or is a soft-delete better for our clients?
- Do we need to build that feature from scratch or can we integrate with a pre-existing service?
- Do we need to create our own user authentication or can we just use OAuth/Cognito/etc?
- Do I need to optimize every method for performance? What parts of the app really need it? Were's the trade off between performance and maintenability?
- Understand when to use relational and non-relational databases depending on the use case. Do we need ultra fast reads/writes? Is data never updated? Do I care if data is lost?
- When to normalize o de-normalize data
- When to use a frontend framework and when not
The following is a non-exhaustive list but outlines some key areas a senior developer should have work experience in or have a strong understanding of:
Source control (Git)
- About version control. Chapters 1 and 2
- The Three States (or trees) of Git: the working tree, the staging area (or index), and the Git directory (or repository).
- How git fully mirrors the remote repository. The .git folder
- Different ways of working with git: Gitflow vs Trunk based development
- Branches
- Good knowledge of basic commands:
add, commit, diff, log, show, reset, checkout, branch, pull, push git reset: soft vs hard- Remotes. Using multiple remotes
- Merge
- Rebase. Introduction Sequence - 4 and Moving Work Around - 2
- Rewriting history. Amend and rebase
- Stashing changes
- Pull request workflow
- Cherry picking
- Merge commit vs squash and merge vs rebase and merge
.gitignore- How to write commit messages
- Verification (e.g. PGP signatures)
- Hooks
- Undoing things & recovering from mistakes
- Resolving conflicts
Basics of algorithms
- Recursion
- Breadth First Search (BFS)
- Depth First Search (DFS)
HTML
CSS
- Selectors
- Display property
- Float
- Positioning elements
- Flexbox
- Responsive design
- SASS vs CSS
- Variables
- Nesting
- Partials
- Modules
- Mixins
- Inheritance
JavaScript
- Arrow functions vs normal functions
- Promises
async/await- modules
this
Testing
- The testing pyramid:
- Unit tests
- Mocks, stubs, and spies
- Creating test data with factories
- Integration tests (end-to-end)
- Dealing with third-party APIs
- Dealing with timezones
- CI and CD
- Test-driven development (TDD), test-driven design, test-first development, test-after development
Object oriented programming (OOP)
- SOLID principles:
- Single Responsibility principle
- Open/Closed principle
- Liskov substitution
- Interface segregation
- Dependency inversion
- Composition vs Inheritance
- Design patterns, including:
- Decorator
- Presenter
- Observer
- Adapter
- Bridge
- Strategy
- Façade
- Factory
- Singleton
- Template Method
- Proxy
- Command
Refactoring
- Code smells (Reek example):
- duplicate code
- dead code
- long method
- shotgun surgery
- long parameter list
- primitive obsession
- feature envy
- "Make the change easy, then make the easy change"
- Extract method
- Extract object
- Understand the dangers of premature abstraction
Performance & scaling
- N+1 queries
- Database query analysis (e.g. EXPLAIN ANALYZE)
- Database indexes
- Where to use them
- Types of indexes
- Database sharding
- Fragment caching
- Russian Doll caching
- HTTP caching (with Nginx, Varnish, etc)
- Measuring
- sample size, significance, etc
- hotspots
- CPU, memory, disk, network
- algorithmic complexity
Security
- SQL injection
- XSS
- CSRF
- Code injection
- Mass assignment
- Authentication
- Authorization
- Timing attacks
- Password complexity
- OAuth
- JWT
- OTP
- How randomness affects security
Architecture
- Inter-service communication in service-oriented systems
- Event-driven systems
- Multi-tenant systems
- Deciding if a single-page application is appropriate
- Model-View-Controller
- Model-View-ViewModel
Preparing for production
- Error monitoring
- New Relic
- Scout
- Rollbar
- Sentry
- Raygun
- Etc
- Observability (e.g. background queue size)
- Handling secrets
- ENV variables
- Encoded secrets
- AWS KMS
- TLS
- Backups (incl. DB, mid-flight processes, securing, verifying)
- Failover
- Deployment
- Heroku
- AWS
- Analytics
- A/B testing
- funnels
- bucketing
- statistical significance
- CDN
- DNS
- A
- AAAA
- CNAME
- MX
- NS
- Nginx
- understand and configure the
nginx.conffile - usage and difference of
sites-availableandsites-enabledfile
- understand and configure the
- Load balancing and horizontal scaling techniques
Speed / Quality trade-offs
- Technical debt
- When it's appropriate to increase debt
- When it's appropriate to pay off debt
Functional programming
- Pattern matching
- Monads
- Error handling
- Non-mutability
Unix
- Process management
- Package managers
apt
- File handling
- Permissions
- The
$PATHENV variable - Shell scripting
- Cron: how to read and configure a cron job
- SSH commands
- Multipart messages (mixed, alternative, related, etc.)
- Protocols
- IMAP
- SMTP
Relational Databases
- How to write an Entity Relationship Diagram (ERD) and how that maps to a database table
- Locking
- Table and Row level locks
- Deadlocks
SELECT FOR UPDATE
- PostgreSQL extensions
- Different types of
JOINs - Aggregating data
- Views
- Materialized views
- Schema design and normalization
- Constraints
- Indexing
- Triggers
- Stored procedures
- Storing JSON/JSONB data
GINindexes
- Handling time and time zones
- Handling geographic data
- Understand the difference between relational database (SQL) vs non-relational (NoSQL). Understand Pros/Cons and when to use:
- Redis
- Mongo/DynamoDB
- Athena
- ElasticSearch
- Graph databases
- Time-series databases
GraphQL
- Schema design
- Type generation
- Avoiding N+1 queries
- Communicating errors
- Apollo
- Federation
HTTP
- Verbs, and verb safety
- Status codes
- URLs
- Cookies
- MIME types
application/x-www-form-urlencodedmultipart/form-dataapplication/json
- Headers:
Cache-ControlEtag/If-None-MatchLast-Modified/If-Modified-SinceVaryX-Requested-With
React
- Components
- Hooks
- State management
- Redux, and when not to use it
- Different ways of handling forms and inputs
- Talking to a server (e.g. Apollo)
Asynchronous systems
- XHR
- Websockets
- Background jobs
- Lambda functions
- Communicating information, results and errors from asynchronous tasks
- Kafka
- SNS / SQS
- Dependencies between asynchronous tasks
Devops
- Infrastructure as code
- Cloudformation
- Terraform
- Docker
- Understand a
Dockerfile - Understand how docker layers work
- Docker compose
- ECS
- Understand a
- Kubernetes
- EKS
Ruby on Rails
Check this first
Please review this Ruby on Rails course first and try to understand if you have any gaps in your knowledge you need to fill in.
Senior developer expectations
This is a non-exhaustive list but outlines some key areas a senior Ruby on Rails developer should have work experience in or have a strong understanding of:
- Having a good understanding of Ruby. Understand the difference between what's part of Ruby on Rails and what is part of Ruby.
- Know how to write Ruby code without rails
- Recognize and understand the differences between a
block,lambda, andproc - Different types of variables: local, instance, class, global
- C optimizations for certain methods
- Understand how Rails is a collection of components/gems that work together:
- ActionController
- ActionView
- ActiveModel
- ActiveRecord
- ActionCable
- Requests/response cycle. What happens behind the scenes when a request is sent: Browser, Routing, Controller, Model, Database
- RESTful routes. https://www.theodinproject.com/lessons/ruby-on-rails-a-railsy-web-refresher
- Testing Rails Applications
- MiniTest vs RSpec
- Factories with factory_bot
- Database migrations
- Why are they useful when working on a team with multiple developers?
- How to perform a Rails version upgrade and Ruby/Rails compatibility matrix
- I18n and conventions
- Polymorphic associations, Single Table Inheritance (STI), and Delegated Types
- Naming conventions:
- filenames: always go in lowercase letters and words separated by underscore
- variables: lowercase letters and words separated by underscore
- classes and modules: PascalCase and no separator between words
- database tables: plural names with lowercase letters and words separated by underscore
- model: singular names with PascalCase
- controller: names in plural with PascalCase
- How to implement caching in Rails
- Debugging
- Benchmarking
- Find bottlenecks
- Routing
- Storing assets: ActiveStorage, Carrierwave
- Skinny controllers
- ActiveRecord callbacks
- ActionController filters
- Asset pipeline
- Hotwire: Turbo, Stimulus, Strada
- Basic metaprogramming in Rails
- Filtering elements in the database vs filtering them in memory
- Counter caches
- Background jobs and understand their differences:
- Rails API mode
- Authentication & Authorization in Rails:
- How a Rails app is typically organized. When does a JS file go to
javascriptand when to thevendorfolder? What does thepublicfolder store? - How to create a gem
- Webservers for Ruby on Rails
- Nginx / Passenger
- Puma
- Unicorn
- Thin
- PDF generation
- PDFKit
- Prawn
- How to handle payments
- Understand how payment webhooks work
- ActiveMerchant
- Eager loading and N+1 queries
- Bullet gem
- Design patterns in Rails
- Service objects
- Value objects
- Form objects
- Query objects
- View objects (Serializer / Presenter)
- Policy objects
- Decorators
- Scaffolding and when it's useful
- What does convention over configuration mean? How is it used in Rails?
- Views templating languages
- ERB
- HAML
- How development, testing, and production environments differ from each other
- How Model View Controller (MVC) works in Rails
- Model (ActiveRecord)
- View (ActionView)
- Controller (ActionController)
- Routing
- ActionCable
- Advanced usage of the Rails console
Node.js
Check this first
Please review this guide first and try to understand if you have any gaps in your knowledge you need to fill in.
Senior developer expectations
This is a non-exhaustive list but outlines some key areas a senior Node developer should have work experience in or have a strong understanding of:
- Arrow functions vs normal functions
- Modules
- using Module.export and exports
- understading the difference between both
- Asynchronous programming: blocking vs non-blocking code
- Promises
- Callbacks
async/await
- Generators
- TypeScript
- Generics
- Node.js APIs:
- Working with files
- Events
- Streaming
- Buffer
- HTTP
- TCP/UDP
- Node.js architecture
- Handling callback hell
- Event driven programming model
- Express
- Routing
- Middlewares
req/reslocals- Session handling
- SQL
- PostgreSQL
- MySQL
- NoSQL
- Mongo
- DynamoDB
- GraphQL
- ORMs
- Sequelize
- TypeORM
- Prisma
- Mongoose
- Apollo
- Authentication & authorization
- Debugging in Node.js
- Testing in Node.js
- Mocha
- Chai
- Sinon
- Faker
- Use of
ENVvariables - Caching
- Redis
- Memcached
- Cluster module
thisobjectsetImmediatevsprocess.nextTickvssetTimeout(fn, 0). Link- External processes
- Starting external application using
process.spawn spawnvsforkvsexecvsexecFile
- Starting external application using
- Offloading tasks that take some time to workers
- Handling JWTs in Node.js
- Handling CORS
- Architectures in Node.js
- MVC
- MVVM
- Hexagonal architecture
- Templating languages
- Mustache
- Handlebars
- EJS
- How to parallelize code with
async - Security in Node.js applications. How to prevent:
- CSRF
- XSS (cross-site scripting)
- SQL injection
- Websockets and real-time communication
- Monolithic arquitectures vs Microservices
- Pros and cons of each one
- When to use one vs the other
- Lambda functions
- Inter service communication
- AWS SQS/SNS
- Kafka
- Package managers
- NPM
- Yarn
- Understand their differences
- Deployment and DevOps:
- Serverless framework
- Containerization with Docker
- CI/CD: Github actions, AWS Codepipeline
- Monitoring and logging in production
- New Relic
- Datadog
- Server side rendering (SSR)
- Next.js
Frontend
Senior developer expectations
This is a non-exhaustive list but outlines some key areas a senior Frontend developer should have work experience in or have a strong understanding of:
- Arrow functions vs normal functions
- Modules
- using Module.export and exports
- understading the difference between both
- Ecma module system
- Asynchronous programming: blocking vs non-blocking code
- Promises
- Callbacks
async/await
- TypeScript
- TypeScript Utilities
- Generics in typescript
- Node.js architecture
- V8 engine
- event queue
- event loop
- thread pool
- garbage collector
- memory management
- Handling callback hell
- Event driven programming model
- Authentication & authorization
- Debugging frontend applications
- Dev tools, Redux dev tools, React dev tools, etc.
- Testing React applications
- Jest
- React Testing Library
- Faker
- Mock Service Worker
- Other testing libraries
- Object
- Object deconstruction and implications
- Object spread operator
- Object.assign
- Object.freeze
- deletions
- Use of
ENVvariables thisobjectsetImmediatevsprocess.nextTickvssetTimeout(fn, 0). Link- Handling JWTs tokens
- Handling CORS
- Frontend Architectures
- Push vs pull
- MVC
- Flux
- MVVM
- Websockets and real-time communication
- Package managers
- NPM
- Yarn
- PNPM
- Understand their differences
- Deployment and DevOps:
- Containerization with Docker
- CI/CD: Github actions, AWS Codepipeline
- Monitoring and logging in production
- New Relic
- Datadog
- Sentry
- Server Side Rendering (SSR)
- Static Site Generators
- State management in React
- Redux
- Context API
- Apollo
- Recoil
- Zustand
- etc
- Caching
- Browser cache
- Server response cache
- CDN cache
- etc
- API State management
- Tanstack Query
- RTK Query
- Zustand
- etc
- Asset bundlers
- Webpack
- Rollup
- Vite
- NextJS
- Turbopack
- Preprocessors and compilers
- Babel
- TypeScript
- SASS
- LESS
- Stylus
- PostCSS
- Frameworks and their difference on how they manage state and rerenders.
- React
- React Native
- Vue
- Angular
- Svelte
- Canvas
- Web workers
- WebRTC
- WebAssembly
- Web Components
- Atomic Design
- CSS
- CSS Animations
- CSS in JS
- CSS modules
- Styled components
- CSS frameworks
- Bootstrap
- Tailwind
- Material UI
- etc
Company culture
Here you will see everything related to our company culture and how do we work.
Trabajo Remoto
Procedimientos, pautas & metodología
Contenido:
- Marco
- Equipos
- Horario - Time overlap
- Check In & Check Out
- Status – Standuply
- Tips para el trabajo remoto
Marco
Este documento tiene como objetivo definir las pautas y los lineamientos a seguir, permitiéndonos organizarnos en contextos de trabajo remoto. Consideramos que para que este esquema sea funcional a nuestra operativa requerimos por parte de cada uno compromiso, madurez y una buena autogestión.
El trabajo remoto debería ser natural, lo incorporamos en eagerWorks como un beneficio y un derecho que se gana en base a la antigüedad en la empresa y sobre todo en base a la confianza generada a lo largo del tiempo.
Podrán hacer uso de este beneficio una vez cada quince días quienes hayan cumplido seis meses de trabajo y una vez por semana todos aquellos que tengan un año de antiguedad. Todos los colaboradores que tengan dos o más años de antigüedad podrán gozarlo sin un límite de días establecido.
Para hacer uso del beneficio deben comunicarlo al menos un día antes a su team lead.
Equipos
eagerWorks puede brindar los equipos necesarios para trabajar de forma remota, coordinando y especificando vía Slack cuáles son.
Quienes prefieran utilizar sus propios equipos pueden conversarlo con su team lead.
Horario - Time overlap
Utilizaremos el horario habitual de trabajo, el cual debe haber sido coordinado de antemano con el team lead.
Consideramos que es importante respetar bajo esta modalidad el horario acordado de trabajo y el de descanso, ya que el desafío más grande de trabajar desde el hogar es establecer dichos límites. Este punto es de vital importancia para una buena gestión y manejo personal.
Para que el trabajo remoto sea exitoso, requiere de cierta superposición con las horas que sus compañeros de equipo están trabajando y con los clientes. De lo contrario, muchas veces podría implicar un retraso entre el tiempo de comunicación o respuesta.
Consideramos que necesitamos unas 4 horas de superposición para evitar retrasos en la colaboración y sentirnos como un equipo, de 11:00 a 15:00 hs.
Check In & Check Out:
Al comenzar la jornada laboral cada uno debe saludar en el canal de equipo de Slack, para dar a conocer a los demás miembros y particularmente al team lead, que se iniciaron las actividades del día y nos encontramos disponibles.
Se entiende que si uno está activo debería estar disponible para recibir mensajes, por lo que es importante mantener la aplicación abierta. Procuremos mantener tiempos de respuesta por debajo de los 15 minutos.
En caso que uno NO esté disponible, se sugiere notificar al equipo, ya sea mediante el status, o con mensajes de AFK (away from keyboard).
Al finalizar la jornada laboral cada persona comunicará que está finalizando las actividades por el día.
Status - Standuply:
Para que nuestros procesos remotos sean exitosos y para estar informados de lo que está haciendo el equipo, utilizaremos standuply mediante Slack.
La idea es especificar todas las tareas que hicimos el día anterior y las que planeamos hacer en el día. Al mismo tiempo podremos mencionar si estamos bloqueados en/por algo.
El trabajo remoto requiere de mayor seguimiento y contacto para minimizar errores, malos entendidos o sobretrabajo.
eagerTips
-
Generar un espacio físico de trabajo apropiado. Preferentemente exclusivo, con condiciones apropiadas a nivel de sonoridad, privacidad, iluminación, conectividad y ergonomía.
-
Evitar trabajar en el dormitorio, en la cocina, o en ambientes en los que es más fácil distraerse.
-
Intentar replicar el mismo setup que en la oficina (monitor, mouse, teclado, silla cómoda).
-
Establecer una rutina: en término de horarios y descansos, para evitar el riesgo de estar sobreconectado o de desconectarse demasiado seguido. Definir los horarios de trabajo y el periodo de descanso.
-
Respetar los horarios de oficina.
-
En las llamadas utilizar auriculares con micrófono para que se escuche mejor.
-
Pedir colaboración de quienes comparten tu hogar para evitar interrupciones.
-
Apagar distractores (redes sociales, correo, notificaciones de celular, etc).
-
Usar un browser dedicado para trabajo y otro para entretenimiento.
-
Vestirse como si fueras a ir a la oficina (evitar trabajar de pijamas).
-
Comer en la cocina, no en el escritorio.