Zero to Sixty: Full-Stack Web Development in Stanford Intro CS
Last fall, I had the opportunity to serve as the head TA for a new introductory CS course at Stanford. As part of this, I worked with the teaching team on designing a new final assignment that gives students a taste of full-stack web development by having them build a miniature social media platform.
This was a fairly challenging undertaking. Most students were taking this class as a first formal CS course, so they had no experience in working with complex codebases. Furthermore, this was not a “how to do web programming” class. It was a programming fundamentals class that introduced web programming at the very end to help students contextualize their learning. As such, it was a significant challenge to walk students through the process of building a web application while ensuring they understood all the moving pieces and felt rewarded for their work.
Before I start, I should highlight that this project was an incredible team effort by the lecturer Jerry Cain and my co-TAs Jonathan Kula, Esteban Rey, Suzanne Joh, and Anand Shankar. Every member of the teaching team had a significant hand in developing the project, and it was amazing to watch it come together over the course of about two weeks.
Context: CS 106AX
Stanford’s introductory CS sequence consists of two classes: CS 106A, which teaches the fundamentals of programming and is targeted at students with no prior experience, and CS 106B, which is an elementary data structures and algorithms class. However, we often have students that don’t fit well into either class: They have seen some programming before and may be bored for the first half of CS 106A, but don’t feel comfortable diving into the fast pace of CS 106B. Alternatively, they may have learned in an unstructured environment (e.g. self-taught R in a lab setting in order to do data analysis) and want style feedback to improve their programming practices before taking CS 106B, which has rigorous expectations.
CS 106AX (106A aXcelerated) was created to improve the experience for these students, breezing through basic programming syntax in just a few lectures but covering the later material of CS 106A in more depth.
The class had two unique features:
- It was taught in two programming languages. Students worked in Javascript for the first four weeks and in Python for the next four, and then we discussed web programming for the remaining two weeks. The goal was to give students experience in mapping universal ideas in programming from one language to another. Ideally, this would reduce the fear of programming languages (non-programmers sometimes ask “how many programming languages do you know?” as if learning many languages is a feat, but most of them are the same to some extent) and give students the confidence to pick up new languages in whatever context they need to after the class.
- We tried to give students a better sense of modularity in design, helping students to break complex code into modules, and figuring out how to get those modules to talk to each other. CS 106A already emphasizes code decomposition from the very first day, but we wanted to take things a step further and look at how systems get decomposed into pieces that run on different computers and communicate over a network. Towards the end of the class, we spent a week giving students a crash course in how the Internet and basic web applications work.
With this capstone project, we wanted to capitalize on both features, having students build a web application in which a Javascript frontend and Python backend work together to produce an experience that we’re so familiar with today.
I should emphasize that this was not a web development class. Stanford has two web development classes (CS 142 and CS 193X) that students can take later in their CS careers. This was an introductory programming class that aimed to teach programming fundamentals, but we simply felt it relevant to contextualize these fundamentals in web applications to cap off the class.
Design goals
Our goal was to give students experience in building a “real,” familiar web application, for two reasons:
- We wanted to give students a feeling of accomplishment for all they’d learned over the course of the quarter. Despite this being the first formal CS class for many of them, they really learned enough to build the basics of a platform people use on a daily basis.
- We wanted students to understand the foundations of how all modern applications work. One of the best ways to understand something in software is to build it.
However, students would need to be able to complete the project in just one week. There are only ten weeks in a quarter, and there were already seven other great assignments before this one!
We decided to have students implement a social network because it’s simple, familiar, and possible to have a working product with minimal functionality. Jerry Cain came up with the idea for “Flutterer,” which would have the following functionality:
- The ability to make new posts on the platform (called “floots”)
- The ability to delete floots they’ve posted (but not other peoples’ floots)
- The ability to comment on floots
- The ability to delete comments
- The ability to switch between users (there needn’t be a legitimate login interface, just some way for someone to toggle between users so that we can make posts and comments from different people)
Students were invited to optionally add extra functionality, like a signup flow or ability to “like” posts.
In designing this assignment, we wanted to minimize the use of libraries and/or frameworks as much as possible. This may seem like an odd decision – who in their right mind avoids using libraries when building something as complex as this? – but it was a very intentional one, for the following reasons:
- With such an ambitious assignment on such a limited timeline, we had a very limited novelty budget. This was students’ first time seeing HTTP requests, implementing a server, doing sophisticated DOM manipulation, building an application with two separate codebases – the list goes on. I felt it was very important to minimize novelty wherever it was not strictly required. As I’ll describe in the next section, we ended up writing our own tiny web server framework, designing a primitive React knockoff, using template strings and DOM manipulation instead of templating libraries, and writing a dead-simple “database” module using a JSON file for persistence. We weighed each decision with the consideration of how much will this deduct from our novelty budget?
- We wanted to minimize the amount of magic in the assignment. In teaching core classes like this one, we focus on teaching fundamental ideas that will remain relevant even if the landscape of programming looks radically different in a decade (which it probably will). We want students to be able to understand how their programs work from first principles. It would be easy enough for students to pick up a proper library after doing this assignment. By contrast, if we had given students a bunch of libraries, they would have been more productive, but would have had a harder time understanding what their code is doing.
Even more importantly, we wanted the assignment to be interesting and fun to implement. We wanted to give students the experience of going from a blank page to a working social media platform. This turned out to be quite challenging, even given the limited functionality target mentioned above.
Developing the assignment
You can try out the finished product here.
Server component
The primary function of the server is to provide API routes that the client can invoke to get data (e.g. get the “floots” that should be shown on the page) or submit data (e.g. post a new floot).
We implemented a tiny server framework for students to use and I’m quite proud of how it turned out. We wanted to abstract away the implementation details as much as possible while doing so in a way that students could understand the code if they wanted to read it, based on material we discussed in lecture. API routes are defined in a simple list of tuples:
# This specifies which functions should be called given a particular incoming
# path. You don't need to understand or change this, unless you're doing an
# extension that requires adding new API routes.
GET_ROUTES = [
("/api/floots", get_floots),
(("/api/floots/(.*)", "floot_id"), get_floot),
(("/api/floots/(.*?)/comments", "floot_id"), get_comments),
(("(/.*)", "path"), serve_file),
]
# This specifies which functions should be called given a particular incoming
# path. You don't need to understand or change this, unless you're doing an
# extension that requires adding new API routes.
POST_ROUTES = [
(("/api/floots"), create_floot),
(("/api/floots/(.*?)/comments", "floot_id"), create_comment),
(("/api/floots/(.*?)/comments/(.*?)/delete", "floot_id", "comment_id"), delete_comment),
(("/api/floots/(.*?)/delete", "floot_id"), delete_floot),
(("/api/floots/(.*?)/like", "floot_id"), like_floot),
(("/api/floots/(.*?)/unlike", "floot_id"), unlike_floot)
]
Then, students simply need to implement the referenced functions. Route
parameters get passed as arguments to API functions, and the POST request body
gets passed as an additional request_body
argument. Functions can return a
string, list, dictionary, or HTTPError, and our server code takes care of doing
the appropriate serialization. For example:
# GET /api/floots/{floot_id}
def get_floot(floot_id):
"""
Given a floot ID, returns the floot if that ID exists in the database, or
returns an HTTPError with status 404 if the provided ID could not be found.
You should return the floot as a dictionary, not as a Floot object (see
Floot.to_dictionary()).
"""
# TODO: delete the following line, and replace it with your own implementation
return HTTPError(501, "api.get_floot not implemented yet")
# POST /api/floots
def create_floot(request_body):
"""
Creates a new floot from the payload information in request_body, which is
a dict that should have the following shape:
{
"message": "contents of floot...",
"username": "name of user",
}
We don't have user-signups on this super simple system, so you can assume
that any username is valid, and any message is valid. However, if the
"message" or "username" keys are missing from request_body (which is
possible; the client might make a mistake and not send them), you should
return an HTTPError with status 400.
When you've saved the new floot to the database, return the floot as a
dictionary (see Floot.to_dictionary).
"""
# TODO: delete the following line, and replace it with your own implementation
return HTTPError(501, "api.create_floot not implemented yet")
For students’ convenience, we implemented hot-reloading in the server code so that the server would reload whenever students’ code changed.
The server needs some way to persist data for this social network. To do this,
we provided students with a “database” in the form of a dirt-simple Database
class that stores floots in an instance variable and serializes/writes that
variable to a JSON file whenever it changes. The code is so simple that
students could easily understand everything it does, but we gave them brief API
documentation and did not require them to read the code.
That’s about it! We tried to minimize the number of things students would need to think about, and since implementing an API route just involves implementing a function, that was pretty simple.
Client component
The frontend portion of this assignment was significantly more challenging to pull off. As mentioned in the design goals, we were strongly opposed to pulling in a framework that would require students to learn a new API. We ended up designing a primitive React knockoff that performs terribly but is quite simple to understand :)
Like real React, everything is organized around components. However, in our version, a component is a function that returns a DOM node:
function ProfilePicture(name, imageUrl) {
let image = document.createElement("img");
image.src = imageUrl;
image.className = "user-photo";
image.alt = "User Profile Image for " + name;
return image;
}
function Floot(floot, loggedInUser, actions) {
let card = document.createElement("div");
card.classList.add("card");
card.classList.add("floot-card");
card.appendChild(ProfilePicture(floot.username,
"img/" + floot.username + ".jpg"));
card.appendChild(FlootContent(floot.username, floot.message));
card.appendChild(LikeCommentCount(floot, loggedInUser, toggleLike));
return card;
}
In our primitive React knockoff, components don’t maintain state. State is only
maintained in a top-level function called Flutterer
. When state changes,
Flutterer
re-renders the entire page by clearing the DOM, passing the
necessary data to MainComponent
(which renders a new page), and adding the
returned DOM node to the DOM.
Is this efficient? Absolutely not. It’s terrible. But is it easy to understand? Yeah, pretty simple.
In order for components to effect change on the page (e.g. the “Post Floot”
button needs to be able to send a floot to the server and re-render the page
with that floot when the server returns a response), we had students declare
closure functions inside the Flutterer
function that make API requests,
update state variables on response, and re-render the page. To avoid a mess of
passing eight different functions through the tree of components, we encouraged
students to declare these functions inside of an actions
object that is
passed to all components that may need to interact with the server or update
state. For example:
let actions = {
changeSelectedUser: function(username) {
selectedUser = username;
updateDisplay();
},
openFlootInModal: function(floot) {
selectedFlootForModal = floot;
updateDisplay();
},
closeModal: function() {
// ...
},
createFloot: function(message) {
// Make API request
// On response, update state var and rerender
},
// ...
};
Then, in a modal component, when the “close” button is clicked, we can do
something like actions.closeModal();
to update state and re-render the page.
Some might be quick to criticize this design. No one does direct DOM manipulation anymore! That’s so inefficient – that will never scale! Global-only state makes life more complex! These are all fair criticisms. However, I argue that this setup cleanly maps to exactly what you’d do with a modern framework:
- Code is organized into components. Our components are just like functional prop-only React components, minus nifty JSX magic.
- Re-rendering the whole DOM may be bloody inefficient, but conceptually, that’s really what React is doing too. Sure, it’s rendering a virtual DOM instead of the actual DOM, and it has some logic to minimize the conditions under which a component rerenders, but the idea is the same, and the simplicity of that idea is one of the reasons it became so popular.
- Students still have to handle state. It’s only in one place, but they still have to think about it.
- Our
actions
object setup is not too far away from actions on a Redux store. - All event handling and logic for sending requests / handling responses is exactly the same as it would be in a production-grade app.
Additionally, despite this design’s simplicity, this was still extremely
challenging to students. If you’ve been programming for a long time, these
patterns may feel natural to you, but control flow here is extremely nonlinear
and substantially more sophisticated than you might expect. On render,
Flutterer()
calls MainComponent
, which calls NewsFeed
, which calls
NewFlootEntry
, which installs an asynchronous click
listener on the “Post
Floot” button
, which (when invoked by the browser) calls
actions.createFloot()
, which bloops control flow back to the closure function
inside of Flutterer
, which kicks off an asynchronous request to the server
(causing their Python code to run), and then when the response is delivered,
the browser calls their asynchronous callback, which sends another asynchronous
request to refresh the list of floots, which eventually triggers yet another
asynchronous callback, which then clears the DOM and calls MainComponent
again to re-render the page.
That’s no joke! Using libraries may have simplified the process of rendering a page, but would have obscured the control flow in a way that made the workings of the program more opaque to students.
That being said, we did run into a few issues as a result of these design decisions. Re-rendering the entire DOM on every state change introduced some quirks; for example, scroll positions reset when the DOM gets rerendered. If you scroll down in the list of comments and then post a new comment, the list will scroll back to the top, because actually, the old list got deleted and it’s displaying a new list that includes the new comment. There may be some clever trick we can employ to fix these issues, but this was minor enough that we did not prioritize figuring out a solution for this assignment.
Despite the quirks, I was happy with how this turned out. I think we got a lot of bang for almost no buck.
Structuring the assignment
In building out this assignment, we first implemented the entire product, then tried to identify the parts we wanted students to implement and worked to figure out the best way to break the assignment into manageable pieces.
This was more challenging than I expected. Despite the small scope of this assignment, there was still a large amount of code involved, particularly in the client implementation. As mentioned in the design goals, we wanted to give students the experience of going from a blank page to a functioning application, but the assignment had to be completable in a single week.
We brainstormed a few approaches, such as giving the students the implementation for posting floots and having them extend the code to add the ability to post/delete comments. However, this would require the students to read through a lot of our code to figure out what it does and how it works, and it would diminish the overall experience, as they might feel as though they’re just tacking on a little piece to a big machine instead of having a hand in bringing it to life. Additionally, if we ask students to implement functionality entirely from scratch, they might spend a lot of time on design or on fiddling with CSS to get the desired results, neither of which are focuses of this class.
Eventually, we decided to give students almost all of the code for the required components. Implementing components is conceptually uninteresting and is mostly just DOM manipulation, which students already got practice with in the previous assignment. Instead, we would have them implement the full logic of the application. This way, we could give them an entirely blank page, and although all the component code would be scaffolded, they could go from blank page to finished product themselves.
We wanted students to feel rewarded as early as possible in working on the assignment. In my opinion, the client portion is the more rewarding part of the assignment, as students get to experience a real user interface come to life. However, I couldn’t think of a good way for them to implement the client first; if the backend were implemented in a compiled language, we might be able to give them a reference implementation to use, but we can’t do that with Python, and CSP prevents us from hosting completed code and having them direct API requests to our server. As such, out of necessity, we had them implement the server component first. Since we did not teach them how to use tools to test API requests without a frontend (e.g. Postman), we gave them a comprehensive unit test suite so that they could validate their server code before moving on to implement the client.
Since the client portion involves a lot of work, we organized the implementation into a sequence of milestones:
- Familiarize yourself with the starter code
- Render the basic interface
- Load the news feed
- Implement user switching
- Post new floots
- Delete floots
- Open floot in modal and show comments
- Create/delete comments
We provided students with a reference implementation hosted on our server. Since there is no authentication, we were concerned about abuse, and added some code to the server so that each IP address uses its own “database” (i.e. JSON file). This way, students will not see posts that other students are deleting, and can feel free to experiment with the reference implementation without worrying about disrupting someone else’s setup.
This assignment culminated in a 25-page monster of an assignment handout. It was extremely long, but it was also very detailed in walking students through the process. We designed the handout (i.e. wrote it, then rewrote it, then rewrote it again, followed by many more edits) so that students wouldn’t need to read the whole handout first, but could instead follow along as they implemented milestones.
Switching between many files is not easy for beginners, and this project involved many different files. In the handout, we tried to include many reminders of where different functions live, and we also included printable cheat sheets with the most important API references and reminders.
Results
You can check out the results:
Some thoughts on how the assignment went:
- In general, the assignment went quite well. Students found the server component relatively easy, and although the client component was challenging, students were able to complete it without taking much more time than we expected. Students reported having fun with the assignment and feeling a strong sense of accomplishment upon finishing it, satisfying our most important design goals. Students also reported feeling more confident about being able to debug complicated code, as well as having a better sense of the workings of programs they use every day.
- Asynchronicity ended up being more challenging than we had intended. As mentioned earlier, students are sometimes expected to write chains of three or more callbacks (e.g. handle click, submit request to create floot, submit request to reload the page), and while some students nailed it, some students really struggled with this aspect. Given the example where students were expected to send a POST request to create a floot followed by a GET request to refresh data, students commonly sent the POST request and then immediately sent the GET request (instead of sending the GET request in the POST request’s callback).
- The “go from blank page to finished product” experience didn’t work as well
as I had hoped, but it probably couldn’t have gone much better given our
short timeline. Although we only gave students the implementations for the
most mundane/boring parts, it was still a lot of code, and the idea of
components was novel for students (as was how things work in a browser in
general). I don’t think we could have minimized “magic” much more, but in
Milestone 2, students go from a blank page to a relatively fleshed-out and
presentable page (using all the components we provided them), and because
that all suddenly appeared without them understanding what our code does, I
think that diminished the feeling of “I built this!” for them. As one student
said, “Since a lot of the code was already given, it didn’t really feel as if
I could implement it on my own, but nevertheless it was probably the coolest
assignment.”
- If we could change one thing in the future, it would be to allocate more time for the assignment. I think we did as well as we could have for a week’s time, but students may have enjoyed the assignment more if they could have implemented more of it by themselves.
- The assignment was heavily scaffolded and clear step-by-step instructions were given in the handout, and while that enabled us to pull off a very ambitious assignment without many road bumps, it also decreased student autonomy. Some students implemented extra features on their own, but those that didn’t felt like the assignment wasn’t very flexible and they didn’t have much creative freedom.
Overall, I am very happy with how this turned out. I think it was a meaningful experience for students and they learned a lot from it without encountering too many rough patches.
Again, this was a huge team effort – every member of the teaching team volunteered to work on this and did a fantastic job of it. The most fun part of this assignment was seeing it materialize so quickly!