I have worked as a web developer for a little over three years, and when I started I assumed the most important skill was simply being able to code. Coding is certainly essential, but it’s only one piece of the puzzle. Many straightforward coding tasks are now automated or assisted by AI, and deploying and maintaining a production web application involves far more than writing code.
Over time, I’ve come to think of web development as a set of interconnected responsibilities. This is my way of breaking down the different parts of a web application and what it really takes to keep one running smoothly. If you’re new to the field, I hope this gives you a clearer picture of what the job involves and helps you prepare for the kinds of tasks you might encounter at work.
This article assumes that you have a basic understanding of what programming is and how the user -> request -> server -> response cycle works in web apps. If you have built a small app locally but haven’t deployed it to production, this is for you.
TL;DR: Programming languages make the app do stuff.
The behavior of the application is defined with programming languages. The main frontend programming language is JavaScript. JavaScript usually handles actions on a page such as clicking buttons, although it may also perform calculations or temporarily store user data to be sent to the backend. HTML defines the structure and content of the page, and CSS defines the styling, but they are not technically programming languages. Ruby and Python are examples of backend programming languages, and SQL (Structured Query Language) is a database query language that you use from the backend. Together, these are used to access the data stored in the database, handle HTTP requests, and write functions that contain complex business logic. There are already many resources for learning to code, so I won’t expand more on this here. Some of the ones I recommend are Codecademy (introductory), The Odin Project and Free Code Camp (free, intermediate) and Launch School (comprehensive coding bootcamp).
TL;DR: Programming languages have different versions which can be managed with a version manager.
Every programming language has different versions and evolves over time. If you have multiple projects that use different versions of the same language, this can be a bit difficult to keep track of. To solve this, most people use a version manager. Ruby has ‘rvm’ and ‘rbenv’, and Python has ‘pyenv’. Some people use ‘asdf’ because it can manage versions for multiple languages. It doesn’t matter that much which one you choose, but it’s best not to use multiple version managers for the same language at once because they can conflict with each other.
TL;DR: A package manager helps you keep track of third-party code so you don’t have to solve problems other people have already solved. When choosing a package, you should make sure it is legitimate and actively maintained.
Imagine you are asked to provide validation that text entered by the user is a valid email address. You could write a function to parse the string and make sure it includes an ‘@’ symbol, has characters before the ‘@’ symbol, and includes a ‘.’ after the ‘@’, but this would still not correctly handle every valid or invalid email address. Rather than solving common problems like this yourself, it’s much better to use an existing package.
A package manager can help you with this in three main ways:
bundle install to automatically install all of them.There are many different package managers, but some common ones include ‘bundle’ for Ruby, ‘pip’ and ‘uv’ for Python, and ‘npm’ and ‘yarn’ for JavaScript.
Packages are a lot like Wikipedia pages, in the sense that anyone can publish them. This is good because there are lots of useful packages that get published, and sometimes other people may add to them and improve them. However, just like Wikipedia sometimes has incorrect information, sometimes packages aren’t very good.
For example, if I search for a “JavaScript email validation package,” I see a mix of old Stack Overflow posts from 2011, unmaintained packages, regex tutorials, and a blog post trying to sell an SDK (Software Development Kit). I can rule most of these out quickly: advice from 2011 is outdated, regex-only solutions are brittle, and packages that haven’t been updated in years could have security vulnerabilities and bugs.
Eventually I find a package with recent releases, and support for installing via my package manager. Before installing it, I check that it exists in the official npm registry, skim the feature list to make sure it does everything I need, and note any potential performance concerns. I then install it on a development branch and test it to confirm it behaves as expected.
Sometimes you already know of a good package from a colleague or friend, but in many cases your search for packages will involve evaluating multiple options. It may be tempting to install the first package you find, but it’s worth taking the extra time to make sure you install the best package for your needs.
TL;DR: Frameworks are like very large packages that solve a whole set of related problems for you.
Frameworks are used in web development because they solve many of the common problems you might encounter when creating a web application. For example, Rails is a Ruby-based backend framework with modules for data storage and retrieval, HTTP requests, sending emails, rendering HTML views, compiling assets, and more! If you had to write code to do all of that yourself, it would take a very long time to get even the simplest web application online. Some other common backend frameworks are Django, .NET, and Laravel.
There are also many JavaScript frameworks that help you create complex, interactive web pages without writing large amounts of custom JavaScript code. They provide a clean way to define reusable, reactive components and keep track of data across multiple pages. Common JavaScript frameworks include Vue, Next.js, and Angular.
TL;DR: Data storage allows user information to persist, even after they log out of the application.
If you’re learning to program, maybe you have created a simple command line game, like blackjack or hangman. It’s fun to play, but it can only store information while the program is running. If you exit the program, the state of the game is lost.
Databases provide a way to save information so that even if the program shuts down or a user logs out, the information will not be lost. For example, this allows a user to make changes to their profile, log out, and then see those changes still in effect when they log in tomorrow.
Most traditional databases use SQL (Structured Query Language), which is a specialized programming language for working with data. Some common databases that use SQL are PostgreSQL, MySQL, and SQLite.
In addition to SQL databases, you may encounter these other types:
TL;DR: Version control helps you keep track of changes to a codebase over time and experiment safely without losing progress.
Imagine your manager asks you to implement a feature that is a bit complicated. You’re not quite sure how you’re going to do it, so you may need to try a couple of different approaches before you get it working. If you just started making changes to the codebase, you could really mess something up with one of your experiments and not be able to get back to the original version!
With version control, you can create a ‘branch’ off of the main codebase, which is just a copy of the existing code. Then you can make changes to it, knowing that your original codebase is not being edited. If one of your experiments goes wrong, you can just reset that branch to get it back to the original copy. You can also ‘commit’ code once you get a small part of it working so that you can keep trying new things without losing the progress you already made. A commit is a lot like saving in a video game. If you die (mess up the codebase), you don’t start over from the beginning, you go back to the last place where you saved.
Git is by far the most common version control system. It has a GUI and integrations in VSCode, but running it in the terminal is usually faster and more reliable.
TL;DR: A code repository allows you to store your code in a central location so everyone can easily add to it.
Imagine you write some code, but you’re stuck on this one really hard problem, and you know someone who might be able to help. Maybe you start a Zoom meeting with them and share your screen, but they can’t figure it out right away and need more time to look at the code. So you zip up the codebase and email it to them, and they email it back the next day with the updated code and some comments explaining what they did. You don’t want to lose the context of what they did, but you also don’t want all those comments cluttering up your codebase. Now imagine you have 10 people working on the same project and how messy this process would become.
Hosting the codebase in a code repository makes the process of collaboration much easier. Everyone can see the current state of the codebase, and they can pull anything that has changed to their local computer when they’re ready. In addition, anybody can open a pull request to suggest changes. A pull request shows the changes, and also allows people to comment on them. A pull request may go through several cycles of review and changes before it actually gets approved and added to the codebase.
GitHub is the most common code repository service, but there are other options like GitLab and Bitbucket. GitHub integrates with git, so you can easily work on a feature branch and push that to GitHub without affecting the main codebase. This way multiple people can collaborate on a feature branch, and once it’s ready it can be merged into the main branch. If a lot of people are working on a project together, it’s important to pull code regularly so your local branch doesn’t get too far behind what is stored in GitHub.
The distinction between git and GitHub can be confusing at first, and it’s important to understand that GitHub does not replace git. You use git locally to track changes and manage branches, and then push your code to GitHub, which hosts a copy of your repository and its branch structure for collaboration.
TL;DR: Every app has secrets and you need a way to keep them safe.
Secrets are things like credentials, API keys, encryption keys, and anything else you wouldn’t want someone outside the project to have access to. Every app has at least some of these things, and just pasting them in the code is unsafe. For example, if you put an API key directly in the code and then upload it to GitHub, it could get scraped and someone could quickly run up a large bill using your API key. Storing them in plaintext in the database is also risky. What you need is a way to store these so that they are out of your source code and stored safely, but still able to be used in your code when you need them.
One common solution is to use environment variables. Environment variables are variables that are defined in the environment where the code is operating. This means that even if you define them locally, you will still have to manually set them on production, and any other environments you may have. Every web development framework will have a way to access environment variables in the code, so if someone else looks at your code they only see a variable name.
AWS Secrets Manager is another common solution which provides a central place to access all of your secrets. You store them using the AWS interface, and then in your code you send a request to the secrets manager to retrieve the value. This is especially helpful if you have multiple applications referencing the same secrets because you only have to store them in one place.
TL;DR: You want your code to be tested so that when you make changes you don’t unintentionally break something.
Imagine you are working on a very large application with multiple areas. In that application, there is a function that is used to calculate discounts which takes three parameters: price, product_type, and promo_code. You are working on the apparel section of the storefront, and your manager asks you to update the website to make sure each user can only use a promo code one time. You add a fourth parameter, user, make other necessary changes to track which users have used what codes, and deploy the code. The apparel section works now, but your manager complains that all the other sections are erroring out every time a user enters a promo code!
Testing prevents scenarios like the above from happening. Some tests check small pieces of code (unit tests), while others test how parts of the system work together (integration or end-to-end tests). If the developers working on the electronics section wrote thorough tests, then you would have seen their tests failing once you added the 4th parameter. At that point, instead of deploying the code, you could have worked with that team to come up with a solution that works for everybody and doesn’t cause any errors for the user.
There are many different testing frameworks, but some common ones are RSpec for Ruby, Pytest for Python, and Jest for JavaScript.
TL;DR: Code linting helps keep your codebase consistent and readable, which prevents small issues from piling up and makes it easier for other people to contribute.
Code linting is the process of automatically checking your code for style, formatting, and common programming mistakes. This usually includes things like the number of spaces for indenting, single or double quotes for strings, preferring a certain built-in method when there are multiple options that do the same thing, ensuring functions and classes aren’t too long, and removing deprecated method calls.
There are many different code linters, and they can usually be customized with various rules depending on what you want your codebase to look like. Some common code linters include RuboCop (Ruby), Flake8 (Python), and ESLint (JavaScript). Linters can be run locally or integrated into your deployment strategy to ensure that code is never deployed without linting.
TL;DR: You need to host your web app somewhere so that other people can access it.
Once you have created your application, the next step is to host it somewhere so that other people can access it. This involves putting your code on a server somewhere, usually in a data center. Some common platforms that charge a monthly fee to allow you to do this are AWS, Digital Ocean, and Heroku. Heroku is a special case because it is actually built on top of AWS, it is just designed to be more user-friendly.
Once your code is on the server, you’ll need to set up your application. Most servers run on Linux, which you’ll access through a terminal. You may need to install the programming language and package manager you used, then use that package manager to install your project’s dependencies. To install the language and package manager themselves, use the package manager that comes with your Linux distribution such as yum or apt.
Once this is set up, you can access your application using the IP address of your server. In order to use a name, you will have to purchase a domain name and use an A (for IPv4) or AAAA (for IPv6) record to point your domain name to your server’s IP address.
TL;DR: You need a way to get new code onto the server where your app is hosted.
Once you have your app up and running, you will inevitably want to make updates. There will be bugs to fix, features to add, packages to update, and all the things that come with maintaining a modern web application. You will need a way to reliably deploy new code to the server so that users can see your changes.
The simplest way is just to pull the new code from GitHub. This is not recommended for large apps with many users, but for a small hobby project it gets the job done. You push your changes to the GitHub repository, SSH into the server, stop the app, pull and merge the new code, then restart the app. The risks are that this is a manual, error-prone process and it also doesn’t give you an opportunity to test the code in a real environment. You may have something that works for you locally but fails on the server, which would cause downtime for your users while you try to figure it out.
Heroku is commonly used because it provides built-in support for deployment. You execute the command, and it automatically restarts your application with the new code. However, even with this setup, you might still have code that doesn’t work on the server even though it works locally. To solve this, many developers will set up two servers on Heroku, one where the real app is that users interact with, and one that is used to test new code. In this context, the two servers are referred to as ‘environments’. The server that has the real app is commonly referred to as ‘production’, and the testing one may be called ‘staging’, ‘testing’, ‘sandbox’, or whatever name your team comes up with. Sometimes there are more than two environments for more complex workflows, but in most cases two is enough.
If you are using a lower-level service like AWS or Digital Ocean, they won’t have built-in deployment like Heroku. Capistrano is a common tool to help deploy your code to these services, but there are other choices too. Capistrano connects to your server via SSH and automatically runs the deployment commands that you define. This is more complex than using something like Heroku, but it is also cheaper and provides more control.
There is no single right way to deploy code, it will depend on your budget, the complexity of your app, the number of users, and the preferences of other people on your team.
TL;DR: If your app is having problems, you want to know about it before you start getting user complaints.
Imagine you have an application that gets Google search data for specified queries once a day for your users. You have it scheduled to retrieve the data at 3 AM every day so the reports are ready when the users check the app in the morning. One day the data retrieval fails, and around 8 or 9 AM when your users start checking the app, you get thousands of emails from users asking why they don’t have the new data yet.
If you had error monitoring in place, you could have gotten an alert as soon as the report failed and had some time to fix it before you started getting emails from users. Some error monitoring services also allow you to track performance metrics and other events in your app. Some popular error monitoring services include AppSignal, Sentry, and Airbrake.
TL;DR: Most of this is handled for you in modern web development, but having a basic understanding can help.
If you are using a modern web framework such as Rails or Django, many security features are already built in. For example, Cross-Site Scripting (XSS) prevention, SQL injection prevention, and password hashing are built into both. However, in order to take advantage of these features you do have to make sure you are using the framework correctly. It is worth taking the time to familiarize yourself with common security vulnerabilities and understanding how your framework of choice handles them.
If you deploy a simple app somewhere like AWS or Heroku, you usually don’t have to think about server-level security in detail. This is because platforms like AWS will handle physical security of the server in their data centers, and modern Linux systems are locked down by default. However, you do need to make sure you don’t carelessly run commands as the root user or expose unnecessary ports to the internet. You are also still responsible for making sure your app itself is secure.
In larger companies, security may be the responsibility of a separate team. They may be responsible for reviewing code and infrastructure changes, or they may perform penetration tests, which is when they purposely attempt to break into your application to identify any vulnerabilities. If they find anything they think is insecure, they may request that you make changes. Even if there is a separate security team, you should still try to make your application as secure as possible.
TL;DR: Documentation helps new developers join the project.
Documentation is an essential part of any web application. There are many different ways to document your application, but in general it’s best to have a small amount of documentation that contains key information rather than documenting every small detail. If you have too much documentation, then nobody will read it and it will quickly become outdated because nobody wants to maintain it. Good documentation is short, practical, and updated as part of normal development.
Some common things that you might find in documentation are the following:
It’s easy to overlook documentation when everyone on the project right now knows how things work and there is no immediate payoff to spending time creating documentation. However, there will be a big payoff when someone new joins your team and they are able to get things up and running quickly instead of constantly asking questions or spending hours figuring things out on their own.