Imagine we are building the next big social app that lets users share content with other users. We will call our app The Snap Gram. This app will put all other apps out of business, so it is very important that we build it well. A flawed design can mean the difference in failure or success for our venture. All it takes is some big company to steal our idea and make it their own. In order to win the tech race, we have to be able to build and ship code faster than everyone else. One aspect that can help is starting with a good design for our software. Let us take a look at how we can architect the front end of our application using Angular.
The Snap Gram will have features similar to most other apps. What we build on the frontend will mimic the data we have on our server. These features/objects will include users, posts, comments, and an admin interface for users to manage their accounts. We can generate a scaffold to get started with our project using the Angular CLI or downloading their Quickstart seed. Alternatively, we can always create the project from scratch. This is what our directory will look like at a high level:
web/
|__package.json |__node_modules/ |__src/ |__index.html |__main.ts |__assets/ |__app/ |__admin/ |__users/ |__posts/ |__comments/
All of the necessary files to build the app have not been listed. Here is an overview of each part:
web/
The root of our project.
package.json
The package manager for our app. At this level, we can also include configurations files, build tools, and a directory for integration tests.
node_modules/
Installed 3rd party libraries including Angular.
src/
Directory of the files we need to edit in the app.
index.html
The entry point of the app.
main.ts
The logic to bootstrap and launch the app.
assets/
Directory for app-wide styles, images, and other static resources.
app/
The directory for app logic. Contains the app root files.
admin/, users/, posts/, comments/
Feature modules of the app.
Where the web directory lives depends on how you are building your app. If you will be using an API that is hosted on another server, then the project can stand alone. If you are the author of the API, you could place this directory within that project where you would normally put public files.
Now we will delve deeper into the app
directory. At the top level of this directory are the files needed to tie together all other parts of the app. Here is an example of the files we need for our root module:
app/
|__app.module.ts |__app.component.ts
|__app.component.html |__app-routes.module.ts
|__not-found.component.ts |__not-found.component.html
This is our root module. It is responsible for bringing together all of the modules and dependencies of the app. Example:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { NotFoundComponent } from './not-found.component';
import { AppRoutesModule } from './app-routes.module';
import { AdminModule } from './admin/admin.module';
import { UserModule } from './users/user.module';
import { PostModule } from './posts/post.module';
import { CommentModule } from './comments/comment.module';
@NgModule({
imports: [
BrowserModule,
AppRoutesModule,
AdminModule,
UserModule,
PostModule,
CommentModule
],
declarations: [
AppComponent,
HomeComponent,
NotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
Imports lists our app's modules, declarations list the components, and bootstrap includes the root component that will be injected into index.html
. Bootstrap only needs to be included in the root module. Partitioning the app into feature modules makes it easier to find and update files because there is a flatter file structure and all related files are grouped together.
This file contains the logic for creating our main view. Example:
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
})
export class AppCompontent { }
The my-app
selector is the element that will be placed inside of index.html
for our view injection. app.component.html
is the base view. This may contain your navigation menu, footer, and other elements that need to persist across pages. We will create other views that will inherit from this view.
This contains all the URL patterns and the views they map to. I recommend defining all of the base file paths in this file and defining the children of these paths in their respective module. Example:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { NotFoundComponent } from './not-found.component';
import { adminRoutes } from './admin/admin.routes;'
import { userRoutes } from './users/user.routes';
import { postRoutes } from './posts/post.routes.';
import { commentRoutes } from './comments/comment.routes';
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'admin', children: adminRoutes },
{ path: 'users', children: userRoutes },
{ path: 'posts', children: postRoutes },
{ path: 'comments', children: commentRoutes },
{ path: '**', component: NotFoundComponent }
]
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutesModule { }
I prefer this method of defining child routes in their feature folder because it keeps related code close and it keeps the main route file lean. Defining all of the routes in this file could potentially become unwieldy. It may not be obvious now, but as you create more URL patterns, it helps to stay organized.
By now you have more than enough code to at least launch a coming soon landing page for The Snap Gram. We could also include a 3rd party signup form on the front page so that we can start building our email list. I definitely want to let interested users know when they can signup. In the meantime, we will begin working on one of our features.
I think the posts feature is the most important one because I want the site to have content and give the appearance that there is a lot of activity. For now, we could let users post without requiring they create an account. We would not have to worry about building the admin or user feature. If we are fishing for comments, we could use a plugin like Disqus. However, if we have a team of developers helping us, we can just have them work on the other features. It should be easy to divvy up the work the way our app is organized.
Let’s look at what is inside of our posts directory. It will essentially mimic the structure of all our other feature folders.
posts/
|__post.module.ts |__post.component.ts
|__post.component.html |__post-detail.component.ts
|__post-detail.component.html |__post.service.ts
|__post.model.ts |__post.routes.ts
This file links together all of the logic related to posts. It is similar to our app module except for a few differences. We still import NgModule, the BrowserModule, and the RouterModule. The PostComponent and PostDetailComponent are added to our list of declarations. And the PostService is included in our providers list.
This is a class for creating post objects. It should have the same attributes as the data we retrieve from our service. This class also serves to define the type for data that uses posts. Here is an example:
export class Post {
constructor( public id: number, public title: string, public content: string ) { }
}
A service should be created to interact with our API or other data source. To use a service, import it into the component you want to use it in and call the methods inside your component class to initialize any attributes or add event listeners to your view. Example:
import { Injectable } from '@angular/core';
import { Http } from ‘@angular/http’;
import { Post } from './post.model';
@Injectable()
export class PostService{
constructor(private http: Http) { }
create(post: Post): Promise<Post> { … }
getPosts(): Promise<Post[]> { … }
getPost(id: number): Promise<Post> { … }
update(post: Post): Promise<Post> { … }
delete(id: number): Promise<void> { … }
}
This will contain routes specific to paths beginning with /post
.
import { PostComponent } from './Post.component';
import { PostDetailComponent } from './post-detail.component';
export const postRoutes: Routes = [
{ path: '', component: PostComponent },
{ path: ':username', component: PostDetailComponent }
]
We have seen one possible way for structuring your Angular Application. Grouping code by feature and designing files with a single responsibility are the main takeaways. This will make it easier to maintain existing features and add new ones as the app grows. I left out adding styles and tests to this app, but that does not mean they are not important. These files should also be kept in their feature folder.
Angular allows for app components to link to css stylesheets. However, you may want to use a preprocessor like Sass or Less. In any case, each partial should still be kept in its feature folder and compiled to a main css file in your assets directory. Unit tests should be kept in feature folders as well. Integration tests or end-to-end tests can be grouped together in a folder inside the project directory. As an exercise, you should use the code examples as a guide to create the other modules. If you have found better ways to improve the design, please share your knowledge in the comments.
In the next part, we will explore routing and navigation.