How to build a good API: Relationships and endpoints
In the first blog post, we wrote about how to design a good API. Now we are going to take the next step and talk about Relationships and endpoints.
Resource relationships in API
There are 3 three main types of relationships between resources:
- One to one
- One to many
- Many to many
One to one
In this relationship, one resource is related to only one resource. In database architecture that would mean that one row in a table may be linked with only one row in another table and vice versa.
Examples of this relationship would be:
- Country has one capital city
- Player has one team
- User has one mobile phone
Generally speaking, tables in one to one relationship could be glued together in one big table.
One to many
Type of relationship that refers to relationship between two resources A and B in which resource A may be linked to many elements of B, but resource B is linked to only one element of A. In database terms that would mean resource A is linked to many rows in resource B, but resource B with only one row from resource A.
- Mother has many children
- Employer has many employees
- Team has many players
We could say that one to many model is always parent-child (children) model, where one element from one resource “owns” many child resource elements. In one to many model it is not possible to “glue” related tables together
Many to many
Relationship in which one resource element is related to many elements (in other resource) and vice versa. Good example would be Movies and Categories relationship.
Movie can belong to many Categories, and Category can have many movies. In relation database management system, these types of relationships are implemented by the means of pivot tables.
Other widely used many to many relationship is User-Role relationships. In which User can have many Roles and Roles can have many Users.
Example for our API project
As we stated in previous article, for our fakeTwitter project we will have three main resources: User, Post, Comment. Now we are going to define relationships between these resources.
From image it is easy to see dependencies between Users and Posts. Users would have many Posts and a single Post will always belong to only one User.
Similarly Posts resource can have many Comments while Comment can belong only to one Post.
Has many through relation
There is also one special type of relationship in which one resource is related to distant resource through third resource.
So in our case, Users resource is related to Comments resource through Posts resource. This relation enables us to get all comments written by one user, which we will use later in the series.
Now we have defined backbone of our API, since API’s are data centers it is crucial to define resources and relationships between them before starting to code.
Practical part
Now we are going to replicate this in Laravel. Resource would be a model, so we begin by creating database structure first and then creating Models folder in app directory and creating User, Post, Comment models inside created folder.
There would be three database tables: users, posts, comments. Easiest way to do this is to create migrations using artisan command:
php artisan make:migration create_users_table
Migrations should look like this:
- Users table migration
class CreateUsersTable extends Migration { public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email')->unique(); $table->string('password'); }); } public function down() { Schema::drop('users'); } }
- Posts table migration
class CreatePostsTable extends Migration { public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->text('post'); $table->integer('user_id')->unsigned(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); }); } public function down() { Schema::drop('posts'); } }
- Comments table migration
class CreateCommentsTable extends Migration { public function up() { Schema::create('comments', function (Blueprint $table) { $table->increments('id'); $table->text('comment'); $table->integer('post_id')->unsigned(); $table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');; }); } public function down() { Schema::drop('comments'); } }
We activate migrations by using the following command:
php artisan migrate
Migrations will create database structure for us, now it is time to communicate with database. Next thing to do is to create Models folder in app folder and create User, Post, Comment model inside that folder.
Good thing to mention when creating custom folders is to declare a namespace that will point to that folder avoiding long and unpractical class references. Namespaces are declared in composer.json under autoload PSR-4 key in project base folder. So my composer.json now looks like this:
"autoload": { "classmap": [ "database" ], "psr-4": { "App\\": "app/", "Models\\": "app/Models" } },
Now we are ready to create models and define relationships between them in the way I described earlier. For handling one to many relationship we are going to use Eloquent’s hasMany() method, for inverse relation (many to one) we will use belongsTo() method and finally for has many through relationship (User – Comments) we will use hasManyThrough() method. The only thing we need to do is just define in on the model and Eloquent magic will do the rest.
So your models should look like this:
- User model
namespace Models; use Illuminate\Database\Eloquent\Model; class User extends Model { protected $table = 'users'; public function posts() { return $this->hasMany('\Models\Post','user_id','id'); } public function comments() { return $this->hasManyThrough('\Models\Comment','\Models\Post','user_id','post_id'); } }
Eloquent’s hasMany command takes 3 arguments (only first is required), first one is related model, second is foreign key on related model and third one is the local key. HasManyThrough() however is little bit more complex, it’s first argument is distant model on which we define relationship, second argument is intermediate model, third is foreign key on intermediate model (Post model) and finally fourth argument is foreign key on distant model (Comments model).
- Post model
namespace Models; use Illuminate\Database\Eloquent\Model; class Post extends Model { protected $table = 'posts'; public function posts() { $this->hasMany('\Models\Comment','post_id','id'); } public function user() { $this->belongsTo('\Models\User','user_id','id'); } }
In this case, we also have a many to one relation which is represented by belongsTo() method, it takes three parameters, first is a related model, second is foreign key and third is local key of parent model (User).
- Comment model
namespace Models; use Illuminate\Database\Eloquent\Model; class Comment extends Model { protected $table = 'comments'; public function post() { $this->belongsTo('\Models\Post','post_id','id'); } }
Similar to previous examples. For more information about relationships in Laravel please visit Laravel documentation.
Defining API endpoints
Now that we have our models defined, we have a blueprint on which we will build rest of our API. As I stated in previous articles, we will use HTTP verb based endpoint naming method. We will use GET, POST, PUT, DELETE verbs. Using this verbs we can define “default endpoints”. Default endpoints are incredibly easy to design and they will exists in almost every project, except default endpoints we will have custom endpoints too.
User model
HTTP verbs | Plural | Singular | Method name | Method name |
---|---|---|---|---|
GET | /users – retrieves collection of User resources |
/users/{id} – retrieves single User resource | getUsers() | getUsersById() |
POST | /users – creates single User resource | – | postUsers() | |
PUT | /users – updates collection of User resources | /users/{id – updates single User resource | putUsers() | putUsersById() |
DELETE | /users – deletes collection of User resources | /users/{id} – deletes single User resource | deleteUsers() | deleteUsersById() |
Last two columns are the names of methods in controller that will handle that endpoint. As you can see, naming examples are quite intuitive and have a consistent pattern.
Post model
HTTP verbs | Plural | Singular | Method name | Method name |
---|---|---|---|---|
GET | /posts – retrieves collection of Post resources | /posts/{id} – retrieves single Post resource |
getPosts() | getPostsById() |
POST | /users/{id}/post – creates single Post resource |
– | postUsersPost() | – |
PUT | – | /users/{id}/post/{post_id} – updates single Post resource |
– | putUsersPostById() |
DELETE | /posts – deletes collection of Post resources | /users/{id}/post/{post_id} – deletes single Post resource |
deletePosts() | deleteUsersPostById() |
In Post model thing are little bit different. Since Post is owned by User model some endpoints are accessible only through User model. These endpoints starts by “users” base node in URL (POST /users/{id}/post, PUT /users/{id}/post/{post_id}).
Some of the endpoints are publicly accessible such as get all posts (GET /posts) or get one post (GET /posts/{id) and furthermore some are accessible only by admin (DELETE /posts – deletes all posts). While designing developer needs to think about usability and functionality which needs to be implemented and adjust endpoints to project needs.
Comments model
HTTP verbs | Plural | Singular | Method name | Method name |
---|---|---|---|---|
GET | /posts/{id}/comments – retrieves collection of Comments,resources | /posts/{id}/comment/{comm_id} – retrieves single Comment,resource for one Post | getPostsComments() | getPostsCommentById() |
POST | /posts/{id}/comment – creates single Comments resource | – | postPostsComments() | – |
PUT | – | – | – | – |
DELETE | /posts/{id}/comment – deletes whole Comments collection,for one Post | /posts/{id}/comment/{comm_id} – deletes single Comment,resource for one Post | deletePostsComments() | deletePostsCommentById() |
Now we have defined most common endpoints that will lead request to our models which in the end will interact with database and do some data manipulation. Bear in mind that these endpoints are just for learning purposes. In a real world example you would have more, depending on your project’s needs. Feel free to have questions about API.