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

Cursor

Visual Studio Code

Sublime Text

Time to configure it

  • If you chose Cursor/VS Code

    1. Create code command

    2. 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 $PATH value to take effect. You'll be able to type code . in any folder to start editing files in that folder.

    3. 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

    1. Create subl command

      sudo mkdir -p /usr/local/bin/ && sudo ln -s /Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl /usr/local/bin/subl
      
    2. Install package manager as shown here

    3. 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.

  1. Install Xcode from the App Store
  2. Open Xcode and accept the licence
  3. 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 doctor and update homebrew with brew 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

  1. Open zshrc:

    nano ~/.zshrc
    
  2. Paste the following:

    export PATH=$PATH:/usr/local/bin
    export PATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin
    
  3. Save those changes and exit.

  4. Apply those changes:

    source ~/.zshrc
    
  5. Finally, type psql in 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.12
    

    List 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 .venv
    

    Activate 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.

  1. HTML stands for Hyper Text Markup Language
  2. HTML describes the structure of Web pages using markup
  3. HTML elements are the building blocks of HTML pages
  4. HTML elements are represented by tags
  5. HTML tags label pieces of content such as "heading", "paragraph", "table", and so on
  6. 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.

  1. CSS stands for Cascading Style Sheets
  2. CSS describes how HTML elements are to be displayed on screen, paper, or in other media
  3. CSS saves a lot of work. It can control the layout of multiple web pages all at once
  4. 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

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:

  1. Uniform interface: requests from different clients look the same, whether the client is a browser, mobile device, or anything else.
  2. Client-server separation: the client and the server act independently and the interaction between them is only in the form of requests and responses.
  3. 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.
  4. 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.
  5. 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.
  6. 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 VerbPathUsed for
GET/photosdisplay a list of all photos
GET/photos/newreturn an HTML form for creating a new photo
POST/photoscreate a new photo
GET/photos/:iddisplay a specific photo
GET/photos/:id/editreturn an HTML form for editing a photo
PATCH/PUT/photos/:idupdate a specific photo
DELETE/photos/:iddelete a specific photo

Learn Databases

A database is an organized collection of data, generally stored and accessed electronically from a computer system.

  1. Create a database
CREATE DATABASE learn_to_code;
\c learn_to_code
  1. Create a table
CREATE TABLE users(
  id serial PRIMARY KEY,
  username VARCHAR (50) NOT NULL
);
  1. Insert data into tables
INSERT INTO users (username) VALUES('alex')"
  1. 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:

  1. Fetch all the favourite items for a given user
  2. Fetch all users together with their favourite items, even if the user doesn't have any items
  3. Get the number of users without favourite items
  4. 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

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:

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 of express.Router(), you create a new 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-validator for 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 .prisma file. 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 generate and 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

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 VerbPathDescription
GET/api/itemsList all wishlist items
POST/api/itemsCreate a new item
GET/api/items/:idGet a specific item
PUT/api/items/:idUpdate a specific item
DELETE/api/items/:idDelete a specific item

Steps to get started:

  1. Scaffold a new Hono project: bun create hono@latest (select the nodejs template).
  2. Install Prisma: bun add @prisma/client and bun add -D prisma.
  3. Initialize Prisma: bunx prisma init — this creates a prisma/schema.prisma file and a .env with a DATABASE_URL placeholder.
  4. Define your models in prisma/schema.prisma and run bunx prisma migrate dev to create and apply migrations.
  5. Create your route handlers. Use app.route('/api/items', itemRoutes) to keep things organized.
  6. Validate request bodies with @hono/zod-validator and Zod schemas.

Exercise 2 — Enhancing the API

After the basic CRUD operations are implemented, let's add more features:

  1. Filtering — As a user, I want to be able to filter wishlist items by name. Add support for a search query parameter on the list endpoint (e.g., GET /api/items?search=book). Use Prisma's contains filter with mode: 'insensitive' for case-insensitive matching.
  2. Pagination — Add page and limit query parameters to the list endpoint. Validate them with Zod (using z.coerce.number() since query params are strings).
  3. Error handling — Return proper error responses (404 when an item doesn't exist, 422 for validation errors). Use Hono's HTTPException and app.onError for 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

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, and data, making it trivial to handle UI states.
  • MutationsuseMutation handles 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:

  1. Scaffold a new React project with Vite: bun create vite wishlist-client --template react-ts.
  2. Install TanStack Query: bun add @tanstack/react-query.
  3. Set up a QueryClientProvider at the root of your app (see the quick start guide).
  4. Create a component that lists all items using useQuery to fetch from GET /api/items.
  5. Create a form component that adds a new item using useMutation and invalidates the items query on success.
  6. Add edit and delete functionality for each item.
  7. 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

AppTechnologyPurpose
apps/webNext.js 16Web application — SSR, caching, Server Components
apps/apiHono + PrismaAPI server — tRPC procedures, database access
apps/mobileExpo + NativeWindMobile application — iOS and Android

Packages

PackagePurpose
@chirp/trpctRPC router and procedures — shared type-safe API layer
@chirp/authBetter Auth configuration — shared auth logic
@chirp/emailReact 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

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:

  1. Static content — plain markup, synchronous computations. Automatically included in the static shell.
  2. Cached dynamic content — data from APIs or databases that doesn't change often. Mark it with "use cache" and control its lifetime with cacheLife.
  3. 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

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 useQuery and useMutation applies 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

Email

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:

WebReact 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

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)

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

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

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.conf file
    • usage and difference of sites-available and sites-enabled file
  • 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

Email

  • 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
  • PostgreSQL extensions
  • Different types of JOINs
  • Aggregating data
  • Views
    • Materialized views
  • Schema design and normalization
  • Constraints
  • Indexing
  • Triggers
  • Stored procedures
  • Storing JSON/JSONB data
    • GIN indexes
  • 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-urlencoded
    • multipart/form-data
    • application/json
  • Headers:
    • Cache-Control
    • Etag / If-None-Match
    • Last-Modified / If-Modified-Since
    • Vary
    • X-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
  • 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, and proc
    • 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
  • 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 javascript and when to the vendor folder? What does the public folder store?
  • How to create a gem
  • Webservers for Ruby on Rails
  • Nginx / Passenger
  • Puma
  • Unicorn
  • Thin
  • PDF generation
    • PDFKit
    • Prawn
  • How to handle payments
  • 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
    • V8 engine
    • event queue
    • event loop
    • thread pool
    • node bindings
    • garbage collector
    • memory management
    • role of libuv in Node.js
  • Handling callback hell
  • Event driven programming model
  • Express
    • Routing
    • Middlewares
    • req/res
    • locals
    • 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 ENV variables
  • Caching
    • Redis
    • Memcached
  • Cluster module
  • this object
  • setImmediate vs process.nextTick vs setTimeout(fn, 0). Link
  • External processes
    • Starting external application using process.spawn
    • spawn vs fork vs exec vs execFile
  • 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:
  • 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 ENV variables
  • this object
  • setImmediate vs process.nextTick vs setTimeout(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.