HOW TO BUILD A RAILS API: AN ART TUTORIAL (PART 1)
Photo by Birmingham Museums Trust on Unsplash

HOW TO BUILD A RAILS API: AN ART TUTORIAL (PART 1)



Step 1: DON'T PANIC

Those were the words on the Hitch Hiker's Guide to the Galaxy, and they are just as apt here as they were there. It is very important that we remember at all stages not to panic. Fear is the mind-killer. Ultimately, if we try to think clearly, embrace problems as things to be solved, rather than terrified at, and persist, we should be able to do anything.

Rails is an excellent framework for creating simple APIs, and once you understand its basic parts, and also the MVC framework in general, building an API should be straightforward and fun. Beyond this, I'm going to assume that you're already a Rails fan, so I don't have to sing its praises or explain what it's used for.

In this tutorial, we're going to create an art-share API (Application Programming Interface), where users can add artworks to their profile, share them with other users, like and unlike them, add them to favorites, and create collections of artworks. It won't be thoroughly fleshed out, but it should be enough to get a feel for the Rails workflow.

That said, I might have to touch a little on how MVC works, so we can ground ourselves for the project.


The MVC Framework

The MVC framework is meant to make it easier to develop well-organized and scalable applications. Basically, there are 3 components to each application, each playing a very specific role.

A diagram of the MVC framework


The Model

This is where the data lives, or rather, the part of the application that connects to the data. If you need to ask anything about the data, then you ask the model.

Let's say your application has users. They could be represented by a table in the database, labeled 'users', with the columns 'id', and 'username'. This can be modeled by a user model, which in this case is simply an interface for connecting with the database. So if I want to get all the users in the database, I would call `User.all`.

The model doesn't care who's asking, or why, or even if they're allowed to ask. All it does is answer the question, provided the answer exists in the database, and it's been taught how to answer: In other words, provided the information you're seeking exists, and a method exists for it. Calling 'User.first_names' won't get me anything because that method hasn't been defined anywhere. That information doesn't even exist.

Part of working with the model is knowing how to work with databases: setting one up, creating, altering, and dropping tables, and populating the database with some seed data. We could write a whole article about this. It's a wide topic. For now, we'll pick up important bits during the project by actually doing.

The View

The view is primarily there for you, the human viewer. Any information the application sends to you will be displayed in some view or other. If it's a web application, it will be HTML on your browser, with some CSS for styling and some Javascript to make it come alive. Views often display the states of models and give you buttons and forms to create, alter, or destroy those models.

But you don't always need a view. It's possible to send raw data back to the user. If the user is another application they don't need an HTML document. An application certainly won't have the same appreciation for a beautiful web page that you or I might have. It will prefer that the data come in a structured format, such as JSON, which it can use to do other stuff.


That's what an API is all about - asking for data from an application and getting it back in a useful format. So, we're not going to be building any views here, just asking for data and getting it back.

The Controller

And finally, we have the brains behind the whole operation. The controller takes requests from the client, asks the model questions on behalf of the client, and then retrieves the information and passes it on to the client, either as a view or as raw data. Sometimes, it can respond by telling the client something went wrong or redirecting them to another location.

We can think of the controller in many different ways. They are the brains, the coordinator, the orchestrator, the traffic controller (maybe not, I like to think the router is the traffic controller, but we'll get to that), etc. Whatever analogy works, really. The point is that questions or requests have to pass through the controller before they get to the model, which then decides whether to pass those questions to the model, how to ask them, and how to respond to the client.

What about routes?

Okay, so let's say we have an application with users, and you want to see a page containing all the users on the app...

How do you ask the app for that information?

Well, normally, if you want to visit a webpage, you enter its address in the address bar on your browser. "https://www.google.com/", for example, takes you to the Google home page. "https://www.dhirubhai.net/in/ramseynjire/" takes you to my LinkedIn profile (Yes, it's a shameless plug, I would love to connect).

The addresses above are known as URLs, and they are like postal addresses where the browser posts its requests. In fact, once we understand the anatomy of a URL, we can start to see how it connects with the MVC framework.


Parts of a URL

Never mind the fancy words naming the parts, what we're interested in for now is the part called 'path'. Everything before 'path' is there to tell the browser how to locate the web server running our rails application. The 'path' tells the webserver where in the application to send your request. The other stuff, like 'parameters', is also important, but we'll touch on that a little later.

When a request hits a web server, the URL is sent to a router for processing. The router looks at the path and decides both which controller to direct the request to, and which action in that controller will handle the request.

A request to display a certain user's profile will be sent to the 'show' action in the users controller, which will find the user in the database and return their data. And a request to delete the profile is sent to the 'destroy' action, which will find the user in the database and delete them, and so on.

On the one hand, the router should be able to match paths, aka routes, with the right controller actions for processing. On the other, the router should have a handbook of which routes to connect to which controller action. This also means that any client sending requests to the web server should ensure they get the path right, or else the router won't know where to direct it. Imagine sending a letter to the wrong address, or to an address that doesn't exist at all!

So, routes ensure we get our requests to the right controller action. The controller then processes that request, talks to the database via the model if it has to, and then sends us back a response.

On the same note, for our API, we'll be working a lot with Postman. Postman is a fun, yet powerful tool for building APIs. We'll use it to form requests, send them to our webserver, and receive responses, which will be displayed in JSON format. Learn about how to get started with Postman here.

With that out of the way, we're ready to build!

Building the Art Share Application

Hopefully, we have a good sense of how the MVC framework works. Now, we're going to use Rails, an MVC framework, to build a simple API that allows users to share artwork. We'll start simple, and then slowly add features along the way.

PS, the code for the finished project can be found on Github. Clone it and look around when you're done with the tutorial. Should be fun to play around with the code and see what happens!

Initializing the App

First, install Rails on your machine. I hope you're using Linux or MacOS. Rails doesn't play very nice with Windows. For this project, I'm using Ubuntu 20.0.4, Rails 5.2.6, Ruby 2.5.0, Postman 7.33.0 (for the desktop version, but you can use the browser version if you want), and Postgresql 12.6 (you can use any database, but I like Postgresql, which you can learn how to install here). I'm also using Visual Studio Code 1.56.2 to do the coding. You can use any code editor, so just find what works for you. If you like, you can learn to install VSCode here.

Note: One thing you'll have to get used to when programming is something I call context difference. Before you can run an app, you need to have all the dependencies ready, in the right versions. I'm doing things in a specific operating system, with specific software, and so on. You either need to get the exact same context, or configure things appropriately for your context, to get the same results I'm getting.

Thing is, if I went through every single contingency about getting things set up right, this tutorial would be a lot longer than it already is. God knows it took me long enough just to get Postgresql running right in my system!

All I can tell you is DON'T PANIC. You'll gain a lot of technical sophistication just by learning to set things up. Google is your friend, in this case, and a healthy dose of patience and determination. I will assume you have everything I have above in the course of the tutorial, so I would advise you to get the dependencies listed. However, if you're on a different system, or decide to get different dependencies, any deviations can be seen as a great opportunity to solve a problem and learn.

Assuming you have all of that, all you have to do is navigate to the folder where you'd like your app to live and type the following

rails _5.2.6_ new app_name --database=postgresql

In this case, my 'app_name' was 'art_share_api_demo', though you can name your app anything you want. Heck you can name it 'the_coolest_app_ever_in_appdom' if you want.

I included flags to specify which version of rails I'm using and also which database. You could alternatively just type:

rails new app_name

This is if you don't care about which rails version and database is used.

PS: The above command will create an app with an empty git repository, simplifying version control. If you're not about that life, you can skip the git repo by including a '-G' flag as below:


rails _5.2.6_ new app_name -G --database=postgresql

Now get into the app folder:

cd app_name

Okay, we have an app running. So... Where do we begin?

Well, first, we launch the app in our code editor:

~/app_name$ code .

Note that you have to be in the app directory to launch it in VSCode. That's why I included the '~/app_name$'. It's a reminder of where we're supposed to be.

My code editor appears as below. Yours should look similar.

No alt text provided for this image


Installing Gems

Usually, after initializing your Rails app, your first stop is the Gemfile. You can see it in the screenshot above on the left. It just says Gemfile, with a bright Ruby gem icon next to it.

This is where you list the gems you need for different environments (There are 3: development, testing, and production, for the different contexts you might want to work in). There are all sorts of gems out there, helping you do all sorts of things.

If you open the Gemfile, scroll down to the part that starts like 'group :development...' Mine looks as below.

No alt text provided for this image

We've got a few interesting blocks here listing gems for different environments. In particular, we want to add a few gems to the development group (the block starting with 'group :development do'):

  • better_errors: This gem gives nice, helpful error pages on the browser when things go awry, as they inevitably will.
  • binding_of_caller: This gem binds methods to their callers higher in the stack, which is fancy speak for 'it will be easier to figure out the root cause of a bug'.
  • pry_rails: Like better_errors, this gem makes things look nicer, except it's your console this time, instead of your browser pages.
  • pgreset: This gem kills and restarts Postgres sessions automatically everytime you try to drop or reset the database, making it easier.
  • annotate: This gem annotates different parts of your app. It can annotate models, showing the corresponding database tables, or your routes file, showing all the app routes.

A lot of your work with Rails will revolve around gems. There's a gem for everything out there. Rails itself is a gem, that consists of a bunch of gems packaged together. Over time, you'll develop favorites, and adding them to your Gemfile will be second nature. Check out the Github pages for the gems above to learn more about them. Meanwhile, your Gemfile should look a little like the one below.

No alt text provided for this image

Note I didn't specify versions, so the latest versions should be installed. If you want, you can install a specific version as well, using the other gems in the image above as an example.

Great! Now that we have the gems we need, we can install them using our handy manager, Bundler. In the command line, run the following code:

bundle install

This should install all the gems in your Gemfile.

And now we can start working on the app. Let's take a look again at our code editor from before.

No alt text provided for this image

A lot of our work will be in the 'app' folder, though we'll also do some stuff in the 'config' and 'db' folders. As we walk through the development, we'll keep coming back to how what we're doing relates to the MVC architecture, just so it all makes sense.

Ready for a wild ride? Yeah, me neither, but let's do it anyway!

Building the Data Models

When you're building an application, it's easier to start by thinking about the data it will work with. If you model the data right, you've sort of half-completed the work. Much of the rest of the application is about connecting users to that data, and allowing them to perform certain actions on it.

That's what we're going to do here. We'll start by thinking of the models we need in our application. This can take a lot of time, depending on the complexity of your app. You will not only have to think about the different data models, but also how they relate to each other, through so-called associations. Take as much time as you need before you commit a single line of code, especially as you get better at understanding databases and models. It will make your work easier down the road. For now, though, we'll assume we've done all the hard work, and just start building step by step.

Users

The first table we'll have in our app is the users table. Everytime a new user joins the app, a new row will be created in this table with that user's attributes. For our app, we'll keep it simple. The only thing we want to know about a user is their username.

There are a couple ways we can generate a users table using Rails generators: We could generate the whole model, which will produce both the migrations file (for defining the database table), and the model file (for defining the model that can access the database table). We could also just generate the migration file separately, and then create the model later. We'll generate the whole model to make things easier.

rails generate model user

You can use Rails generators to do lots of things right in the command line, reducing the amount of code you have to write later. Learn more about 'rails generate' here.

Now we have the foundations of a user model. The first stop is the migration file.

No alt text provided for this image

As you can see, this file can be found at db/migrate/migration_file_name.

Rails migrations inherit from ActiveRecord::Migration, as you can see in the image, which has lots of handy methods that let us use Ruby to design our database. You can learn more about migrations here and here.

In this case, we'll want to define a username column for our users table. We also don't want users without usernames, so we'll add a constraint making it a must for all users to have a username.

No alt text provided for this image

We also want users to have unique names. Things would get really complicated otherwise. So we'll add a uniqueness constraint, as shown below:

No alt text provided for this image

As you can see above, we made usernames unique by adding an index. Adding an index to a particular column in a database makes searches faster. They get even faster when you only allow unique values for that column. This is because an index enables the creation of what we call a binary search tree. It's a bit of a wide topic, so I'll leave you with this resource to explain it a little better than I can.

This will be a common pattern when you're creating tables. Everything is done within the block that starts with 'create_table :users do'. There you can define the columns and types using methods from the ActiveRecord::Migration class. For example, above, we get to define the type of column with 't.string', the name with ':username', and added a bunch of options. So the overall pattern is 'datatype name options'.

It seems we have everything we need here, so the next step is to run the migration so the table is created. First, though, we need to create the database by typing the following into the command line:

rails db:create

You should remember to do this every time you create a new Rails application, otherwise, none of your migrations will run.

Once the test and development databases are created, type the following:

rails db:migrate


Now we have a users table in our database! How do we know that? well, if you look at the 'db/schema.rb' file you will see that you now have a users table with the attributes we defined in our migration.

No alt text provided for this image


We have now created users in the database. But how can we interact with that database? How do we create new users, edit the attributes of existing users, and delete users? We obviously don't want our users sending raw SQL queries via the API. We need an interface that can connect us to the database. That interface is the model layer. Or, in Rails lingo, Active::Record.

Active::Record is a so-called ORM (Object Relational Mapper), thanks to the intermediary role it plays. It has a whole slew of interesting capabilities and features. You could write a whole book on it. We have already interacted with it by writing a migration file. Now, we're going to use it to create models, which we can then write validations and associations for. You can learn more about Active Record here and here.

To create our model, we need to open the file 'app/models/user.rb'. It was created for us already because we used 'rails generate model user' earlier. Had we just created a migration file with 'rails generate migration create_users', it wouldn't have been created, and we'd have had to do it ourselves.

The first step when you open a model file is to write validations. In this case, we're validating that usernames exist and that they are unique. Our validations will mirror the database-constraints we wrote in our migration file when creating the users table.

No alt text provided for this image


Why do we write validations? If we already wrote database constraints, why do we need to validate the same stuff in the model? When you try to create a user via the model and don't have validations, the model will try to run SQL and create a user in your database, and then get an error. Let's try it.

Comment out the validations above:

No alt text provided for this image


Now let's get into the rails console. Type the following in the command line:

rails console

Now try to create a user with the #create! method

User.create!

You should get an output like the following:

postgres NOT NULL violation

Now, let's go back to the model file and uncomment the validations. We will now go back to the command line and reload our console:

reload!

Now run the 'User.create!' code again. Your output should look as below:

No alt text provided for this image

See where it says "Username can't be blank". This is so much nicer to read than the scary output we had before. Moreover, it can be sent to the user through the method 'user.errors.full_messages'.

Another reason we write validations is to cover our backs. When a user interacts with your app via a visual interface, such as a web browser, you can add client-side validations to check that the username isn't blank, or that a password has a certain number of characters, and so on. But you also want validations on the backend, in case they're using an API, or they're probing you for weaknesses and try to hit your app via the command line. In that case, model-level validations should stop them in their tracks. Database validations are the final frontier. If all the other validations are somehow avoided, you want to make sure that the data that finds its way into your database maintains its integrity.

Validations are pretty interesting, and there is no end to what you can validate. You can even write your own validations. Check out this resource to learn the basics, this one to learn how to write custom validations, and this one to go further in-depth.

We can now test if our user model works. Remember, every user should have a username, and usernames should be unique. You can test this in the console.

rails console

Now you can try and create a user with no username:

No alt text provided for this image

Or you can try to create two users with the same name:

No alt text provided for this image

Since we're not writing tests in this tutorial, you'll have to test everything manually like this. In a future tutorial, we'll explore writing tests for our Rails apps.

We won't be writing anything more in the user model now. Instead, we shall seed the database with a few users. That way, it will be easier to play with them later without having to create new users. Go to 'db/seeds.rb'. This is where we write code for seeding the database.

No alt text provided for this image

I used the names of my close friends here. You can use whichever names you like. Now, we reset the database so it recreates itself with our seed data.

rails db:reset

You can now test that your seed users are there in the database. Type 'rails console' (or the shorter 'rails c') and use 'User.first' to check the details of the first user, 'User.second' to check the details of the second user, and so on.

Once you're satisfied that we have a proper working user model with seed users, take a step back and breathe a sigh of relief, because we just finished our first major milestone! We now have a sense of how things work when creating models, and creating the others should be pretty easy!

Artworks

Our next order of business is to create an artwork model. This will represent data pertaining to the artworks that the users share with each other.

Our regular flow will be:

  • Generate a model
  • Write the migration, including database-level constraints
  • Run the migration
  • Write the model, including model-level validations and other code
  • Test that the models are working in the console
  • Write seed data and reset the database

So let's generate our model:

rails generate model artwork

Next, we write the migration that will create the table at 'db/migrations/[your unique migration]. But wait, what attributes will our artworks table have?

Again, in your own work, this is where you'd do the brainstorming. In this case, however, let's just assume that we've already done that. So we'll need a title, and an image_url, which is a link to where the actual artwork lives (we won't store any actual artworks in our database to avoid complicating things for now). It will also need a foreign key referencing the user that created the artwork - the artist. None of these can be blank.

Foreign keys allow relationships to be defined at the database level. Basically, each artwork has an artist_id that represents an id in the users table. In that way, a user can have many artworks. We will then later write these associations at the model level, leveraging Active Record's powerful association methods. To learn more about foreign keys and associations, check out this resource.

We will also want to make sure that a user cannot create two artworks with the same title. Two different users should be able to create artworks with similar names, though. It should also not be possible for two artworks to have the same image_url. Finally, we want to be able to quickly search for a particular user's artworks in the artworks table. All of these problems can be solved with an index. Once again, don't forget to check out this resource to learn more about indexes and uniqueness.

All the above can be achieved with the following migration:

No alt text provided for this image

When creating an index involving two different columns, you put them in an array, as in 't.index [:title, :artist_id], unique: true'. That ensures a user cannot create two artworks with the same title. You can read more about this on this Stack Overflow page. Another interesting line is the 'user: :references' option for the artist_id column. This tells the database that the artist_id is pointing to the users table.

You can now run the migration:

rails db:migrate

Now we open the model file at app/models/artwork.rb. Here will write validations for presence and uniqueness to mirror the constraints we imposed at the database level.

No alt text provided for this image

But what about making the combination of title and artist_id unique? For this, we will use the scope feature. Basically, it allows us to restrict the uniqueness validation to a given scope. So we can validate titles for uniqueness, but only for titles that share an artist_id. We can even add a helpful error message in case a user fails the validation. You can read more about this in Rails Guides.

No alt text provided for this image

Note that there is a comma after ':artist_id' since the scope and message are keys in a hash. Don't forget that comma when writing hashes like this. I know I do. Don't be me.

Also, the message is written a little incompletely. When the error prints it will actually read 'title cannot be same for two artworks by the same artist'. Error messages for any attribute you're validating will usually have the name of the attribute prepended. This is why you should write the messages less the attribute name at the beginning, to avoid having it twice.

You can now test that the above validations work in the console. Just run 'rails c' and then try to create faulty artworks, without titles and artist_ids, similar titles but same artist_id, and so on. For now, you'll have to just write random artist ids, since we haven't written associations between artworks and users. That's what we'll do next.

Rails associations are powerful things. Did you read this resource? If you haven't, have a quick look at it. With Rails, you can define has_many, belongs_to, has_and_belongs_to_many, has_one, and 'has_many :through' associations. In this case, we know that an artwork belongs to a user, and a user can have many artworks. How do we connect them? With the foreign keys, of course!

The Artwork model should now look like this:

No alt text provided for this image

As you can see, belongs_to is a method with a few arguments. We can specify the name of the association semantically for easier understanding. An artwork belongs to an artist, even though its foreign keys point to the users table. To help Rails find that table, we specify class name, the foreign key to check in the artworks table, and the primary key it is referring to in the users table. We could write anything instead of 'artist', such as 'creator', etc. The most important thing is that we use the rest of the hash telling rails how to find that 'artist' or 'creator'.

By the way, in Rails, belongs_to associations validate the presence of the defined foreign key by default. In this case, that foreign key is 'artist_id'. That means we can delete 'artist_id' from the presence validation at the top, or else we will have repetitive error messages (one based on the validation you wrote, and another based on the default validation run by the belongs_to method). You can check this for yourself by trying to create an artwork without an artist id before and after removing 'artist_id' from the validation you wrote. Once you're satisfied it causes repetitiveness, remove it so your artwork.rb file looks like this:

No alt text provided for this image

Associations work both ways, so we need to write a has_many association in the user.rb file.

No alt text provided for this image

The structure is almost the same as with the belongs_to association. In this case, the foreign_key is the key referring to the users table from the other side, which is 'artist_id'. We also added a little option called 'dependent: :destroy'. This instructs Rails to destroy all records of artworks by a user when that user is removed from the database. This avoids cases where we have orphan artworks.

We should now be able to use methods like user.artworks or artwork.artist to find a user's artworks and an artwork's creator, respectively. Open the Rails console to test this:

rails console

In this case, we already have some users in the database, since we created them in the seeds file. You can assign them to variables to make them easy to use. My code looks like this:

No alt text provided for this image

Now I can create an artwork with ramsey as the artist:

No alt text provided for this image

In this case, I leveraged the variable I had created earlier to assign an artist id as 'ramsey.id'. Don't forget to include all the attributes when creating your artwork, and write them appropriately too (strings should be within quotes, for example). Now, when you query ramsey.artworks or first_artwork.artist, you get appropriate results.

No alt text provided for this image

Assuming all of this is working for you, we can now update the seeds file to create some artworks...

No alt text provided for this image

And then reset the database...

rails db:reset

You can test that the artworks and associations have been created in the console as usual. When you're done poking around, take another deep breath. We've completed another major milestone!

Sharing Artworks

Now we're getting to have more fun. We have users and artworks, and have set up the models such that each artwork has a creator and a creator can have many artworks, as well as a bunch of other stuff.

Now, what if a user wants to share an artwork with another user? In this case, we have to think of a share as a sort of entity itself. Every time a user shares an artwork with another user, a record is created, with the ids of the artwork being shared and the user it has been shared with. This way, an artwork can be shared with many users, and a user can have many artworks shared with them.

In database lingo, we call this a join table. It will have two foreign keys: an artwork id and a viewer id, referring to the artwork and the user it has been shared with, respectively.

We should make sure that both ids are present every time a share object is created. We should also make it such that an artwork can't be shared with the same user more than once. We should also add an index to each of the foreign keys to speed up searches for shares based on either the artwork id or the viewer id. Again, don't forget to read up on indexes.

We can name our model ArtworkShare, to make it easier to recognize. Now that we know what kind of attributes and constraints it has, we can build it.

Remember...

  • Generate a model
  • Write the migration, including database-level constraints
  • Run the migration
  • Write the model, including model-level validations and other code
  • Test that the models are working in the console
  • Write seed data and reset the database

So...

rails g model ArtworkShare

Then we write the migration...

No alt text provided for this image


Note that we don't require each artwork_id or viewer_id to be unique, only the combination of the two (the line that reads 't.index [:artwork_id, :viewer_id], unique: true'). That's what will prevent the same artwork from being shared with the same user more than once.

We can now run the migration:

rails db:migrate

Now it's time to write the model. First, we write the validations:

No alt text provided for this image

Note, we didn't write presence validations for artwork_id and viewer_id. This is because we shall write belongs_to associations here, which validate the presence of the associated foreign keys by default.

In this case, an artwork share belongs to a user and an artwork. An artwork can have many artwork shares and a user can also have many artwork shares.

How do you know where to write the belongs_to and has_many associations? Simple, whichever table has foreign keys belongs to whichever table its keys are referring to. Since the artwork share table has foreign keys referring to both the artworks and users tables, it belongs to both of them.

So first we'll write the belongs_to association:

No alt text provided for this image

And then the has_many association for the artwork:

No alt text provided for this image

And the same for the user:

No alt text provided for this image

Note we included the 'dependent: :destroy' option on both to avoid orphan shares.

And now we come to the part where we understand why artworkshares is a join table. Basically, it allows us to find all the viewers whom a particular artwork has been shared with. Just check all the viewer ids that are paired with the artwork's id, find the associated users, and return them. Ditto for finding all the artworks shared with a user. This uses an SQL JOIN query under the hood, though it's possible to write associations that tell Active Record how to execute that query. In this case, we shall use a 'has_many :through' association. Read more about join tables and the 'has_many :through' association here.

An artwork has many viewers through artwork shares. A user has many shared artworks through artworks shares. How do you tell Rails how to build this association?

For artworks:

No alt text provided for this image

For users:

No alt text provided for this image

For example, the method in the user model is telling Rails 'I have many shared artworks through my artwork_shares association. The source of that information is the artwork association (inside the artwork_share).'

You can now open the Rails console and create a few artwork shares. Be sure to test both the validations and associations to make sure everything works. Once you're sure it does, we'll write some seed data.

No alt text provided for this image

And then reset the database:

rails db:reset

Conclusion

And with that, we have completed part 1. We have created database tables and the models that wrap around and communicate with them. We have built the 'M' in 'MVC'. In the next section, we will head to the 'C' and do more fun stuff. Hoping to see you there!

This tutorial is based on this project on App Academy. You can build it yourself if you want. It's loads of fun!

Any questions, or just wanna connect? Message me on my profile.


要查看或添加评论,请登录

Ramsey Njire的更多文章

社区洞察

其他会员也浏览了