A Logistics application is designed to facilitate the movement of goods from one place to another. The advantages of a real-time logistic application such as improved security, enhanced customer experience and transparancy cannot be overemphasized.
In this tutorial series, we will learn how to build a real-time logistics application using Strapi as our backend, Angular as our frontend, and PostgreSQL as our database. We will also integrate Leaflet for Geolocation and mapping. Finally, we will convert our application to a Progressive Web App(PWA).
This tutorial will be divided into two parts:
Throughout the Part 1 of this tutorial series, we will look at how to set up the backend of our logistics application using Strapi and PostgreSQL. We will create Strapi Content Types and configure permissions to manage access effectively. On the frontend, we will build the application using Angular, and implement robust authentication and authorization mechanisms.
Below is a demo of what we will be building:
Strapi is a Headless CMS for developers that makes API creation easy.
Apart from Strapi being a powerful cms, it is easy to setup and integrate into any frontend framework. It also reduces the amount of time required to create APIs from scratch.
Let's start.
To get started with building our logistics application, ensure you have the following installed:
Strapi supports most relational databases like SQL, SQLIte, MySQL, and PostgreSQL for your project.
For this project, we will use PostgreSQL. You can install PostgreSQL here.
Next, we will create a new user and database on pgAdmin. For this project, we will be calling our database logistics_db
. Create a username and password according to your preferences, but make sure to remember them for your strapi setup.
To create a new user and database, follow these steps:
sudo -u postgres psql
If you are using the Windows operating system, open the command line interface(CLI) and run the following command:
psql -U postgres
You might be required to enter the password; make sure you remember the password you created during installation.
1CREATE DATABASE logistics_db;
2CREATE USER 'your username' WITH PASSWORD 'your_password_here';
3GRANT ALL PRIVILEGES ON DATABASE logistics_db TO Nancy;
1\q
Now, we are done with our database setup, and we have successfully created a username and database.
To install Strapi, open your command line or terminal and run the following commands:
npx create-strapi@latest
Or, if you want to install Strapi globally
npm install strapi@latest -g
To create a new Strapi project, run the following command:
strapi new logistics-backend
During the setup process, you’ll be prompted to choose a database. Select PostgreSQL and enter the necessary database credentials (you will use these to configure PostgreSQL later).
To start your Strapi project in development mode and see Strapi in action, run the following command:
npm run develop
This will start the Strapi development server and automatically redirect you to the Strapi admin panel. You should see output in the terminal that indicates Strapi is running. If it doesn't do that, click on the link to take you to the admin panel.
Next, you will have to provide your credentials to register as an administrative user.
Once you click on "let's start", you will be redirected to the Strapi admin dashboard.
Once you have Strapi up and running, you can verify that the connection to PostgreSQL is working by:
logistics
) using a PostgreSQL client (e.g., pgAdmin or a terminal client) to confirm that tables are being created.That’s it! we’ve now set up Strapi with PostgreSQL. we can start building our content types and models, configure your backend, and develop the API for the logistics app.
You can learn more about how to connect Strapi to PostgreSQL.
Now that we have set up Strapi, it is time to create the content types for our application. We will be creating four main content types:
By default, strapi has a User Content type.
The driver in the logistics application is responsible for taking orders to their destinations. The Driver content type holds details about the drivers, the shipment and orders they are assigned to.
The Driver content type should have the following fields:
shipment
: Relation with Shipmentorder
: Relation with Orderuser
: Relation with User (from users-permissions
)driverId
: NumberYour Driver collection type fields should look like this once you're done creating them.
The Order content type holds all the details about the orders created by a User.
The Order content type should have the following fields:
senderName
: TextreceiverName
: TextdeliveryInstructions
: TextitemDetails
: Text shipment
: Relation with Shipmentdriver
: Relation with DriverorderId
: NumberYour Order collection type fields should look like this once you're done creating them.
The Shipment content type holds a the details about the orders with the drivers they were assigned to including tracking the shipment status.
The Shipment content type should have the following fields:
order
: Relation with OrdershipmentStatus
: Enumlongitude
: Numberlatitude
: Numberdriver
: Relation with DrivershipmentId
: NumberYour Shipment collection type fields should look like this once you're done creating them.
By default, Strapi has a User Content type. It generates it through the users-permissions plugin. To make use of the User content type in our logistics application, we have to create relationships between the user content types and other content types. To do that, let's follow these steps:
By default, Strapi allows two types of roles through the user permissions plugin. The "Authenticated" and "Public" roles.
Public users are those who are not authenticated using Strapi JWT or the API token and cannot access APIs that require authentication. Authenticated users use the JWT or API token to make API requests.
So now, we are going to configure permissions for the two types of users. To do that, we go to Settings > USERS AND PERMISSIONS PLUGIN > Roles.
Public users have to be permitted to register and log in.
For authenticated users, we will give permission to be able to find Users.
Allow find
and findOne
the Order collection type.
Allow find
, findOne
and Update
for the Shipment collection type.
After creating our content types and configuring permissions, the next thing to do is to start creating entries.
Strapi provides us with a feature called Draft and Publish. A Draft
label is for an entry you're still working on, while the Publish
label is for content that is ready for use.
Go to the "Content Manager" on the left hand side of the admin panel, select the "Driver" collection type and click "Add New". Fill in the Driver details like the Driver ID, User, and link the Shipment fields by selecting the relevant shipment and click on "Save".
Continue by adding other entries for the Order, Shipment, and User collection type.
Angular is a front-end framework for developing single-page applications. It is built around the component-based architecture and dependency injection. Hence, Angular provides a more organized, modular way of development of web applications. Angular uses typescript, which improves code reliability and efficiency.
To create an Angular project, we will use the Angular CLI, a command-line tool for setting up and managing Angular applications. This command will generate a new Angular application scaffold for our logistics app.
To install Angular, run the following command:
npx -p @angular/cli@latest
To create a new Angular Project, run the following command:
ng new logistics-frontend-angular --routing --style=scss
This will create a new Angular project called logistics-frontend-angular
with built-in routing and SCSS for styling.
To navigate into your project directory, run the following commands:
cd logistics-frontend-angular
To start your server locally, run the following command:
ng serve
To install the dependencies that will be used in this project, run the following commands.
npm install leaflet lucide-angular
npm install --save-dev tailwindcss autoprefixer postcss
npx tailwindcss init
Explanation of Dependencies:
The next thing we are going to do is to create our folder structure. We will be needing interceptors, models, pages, services, spinner, and environments folder for our logistics application.
cd src/app
mkdir interceptors models pages services spinner environments
The first command will navigate to the src/app
folder, while the second command will create the folders accordingly.
Now let's understand what each file and folders do:
src/app
Folder:This is the core of our application. The src/app
folder contains all components, services, models, and our routing configuration. In the src/app
folder, you will find the following files:
app.component.ts
file: This file contains the root component of our application and hosts the main layout of the app.app.component.html
: This file contains the HTML template for the root component.app.component.scss
: The file contains the styles for the root component.app.component.ts
: The file holds the logic for the root component.app.module.ts
and app-routing.module.ts
: Theapp.module.ts
file contains the main Angular module that bootstraps our app while app-routing.module.ts
file contains the routing configuration for navigating between different pages of our app.interceptors/ Folder
This folder contains interceptors that handle HTTP requests globally, such as adding authentication tokens or handling errors.
auth.interceptor.ts
: This file handles adding authentication tokens to HTTP requests and handling errors. To create this file, run the following commands:
ng generate service interceptors/auth
pages
FolderOur pages folder contains components that represent different pages or views in our application.
Create the following pages for this folder by running the commands below:
ng generate component pages/driver/driver
ng generate component pages/driver/driver-dashboard
ng generate component pages/driver/driver-login
ng generate component pages/driver/driver-register
Here is what we will do with the pages generated above:
driver.component.ts
file: Our <router-outlet></router-outlet>
is included heredriver-dashboard.component.ts
file: Contains the page for the driver's dashboard to view and update the shipments assigned to them.driver-login.component.ts
file: A simple driver login page.driver-register.component.ts
file: A simple driver registration page.landing-page.component.ts
file: This file contains the landing page where new drivers can register, and existing drivers can log in.track-order.component.ts
file: This file contains the page for tracking orders by customers.To create the landing-page.component
and the track-order.component.ts
file:
ng generate component pages/landing-page
ng generate component pages/track-order
services
FolderThis folder contains services that manage business logic, API requests, and shared functionality; in the case of our logistics application, it houses several methods for our app to function.
To create the necessary service files, run the following commands:
ng generate service services/api
ng generate service services/auth
ng generate service services/geolocation
ng generate service services/user
api.service.ts
file : Handles API requests related to orders, shipments, and driver management.auth.service.ts
file: Manages authentication (login, registration, token management).geolocation.service.ts
file: Holds geolocation data and related functionality.user.service.ts
file: Manages our user-related data (including driver data).spinner
FolderInside the spinner
folder, there is a spinner.html
, spinner.css
and spinner.ts
file. The loading spinner component in the spinner.ts
file indicates when data is being loaded e.g when finding an order by orderId
.
We need an API Service that will be a backbone for sending and receiving data. It will be the channel through which all our API requests will be routed to our backend, Strapi. It will also handle authentication and error handling for our project.
To set up our API service, we need to define the service using Angular's @Injectable()
decorator, making it available for injection throughout our app.
1@Injectable({
2 providedIn: 'root',
3})
Next, we set up the baseUrl
variable, which holds the base URL for our Strapi API. This value is fetched dynamically from our environment configuration, so we can easily change the server's address based on the environment (development, production, etc.).
1private baseUrl = `${environment.strapiUrl}/api`;
To make HTTP requests, we inject Angular’s HttpClient
into our service using the inject()
function, which makes the code cleaner. we can also decide to go with the constructor based DI(dependency injection).
1private http = inject(HttpClient);
When building web apps, errors are bound to occur, which makes error handling an important feature of every application. We will use the handleError()
method, which ensures that any error during an API request is properly handled to handle errors in our ApiService
. Instead of letting our app crash, we log the error and throw it back so it can be dealt with appropriately.
1private handleError(error: HttpErrorResponse) {
2 console.error('API error:', error);
3 return throwError(() => new Error(error.error || 'An error occurred.'));
4}
This way, we ensure that when something goes wrong, we don’t just get stuck. Instead, we get a clean log to investigate and handle the error effectively.
One of the main tasks in our logistics application is tracking orders. We do this through the trackOrder()
method. When we want to retrieve information about an order by its orderId
, we make a GET
request to our Strapi backend.
1trackOrder(orderId: number) {
2 return this.http
3 .get<any>(
4 `${this.baseUrl}/orders?filters[orderId][$eq]=${orderId}&populate=*`
5 )
6 .pipe(catchError(this.handleError));
7 }
Explanation:
When a user wants to fetch data, we send a GET
request on our Strapi backend using the orderId
. The populate=*
parameter ensures that we retrieve all related data (like customer details, order items, etc.) in one go. If there's an issue with the request, the catchError()
function catches it and hands it over to our handleError()
method to handle the error.
The getAssignedShipment()
method helps us retrieve shipments assigned to a specific driver. It works by sending a GET
request to our Strapi backend using the driver's driverId
.
1getAssignedShipment(driverId: number): Observable<any> {
2 return this.http
3 .get<any>(
4 `${this.baseUrl}/shipments?filters[shipmentId][$eq]=${driverId}&populate=*`
5 )
6 .pipe(catchError(this.handleError));
7 }
This method works by us sending the driverId
in the request to our Strapi backend, which returns all shipments assigned to that driver. Like the order tracking request, we use the populate=*
parameter to ensure related data (such as shipment) is included.
The updateShipmentStatus()
method is essential for updating the status of a shipment, such as when it’s delivered or in transit. If the driver is also providing location data (latitude and longitude), we include those as optional parameters.
1updateShipmentStatus(
2 driverId: number,
3 status: string,
4 lat?: number,
5 lon?: number
6 ): Observable<any> {
7 const data = {
8 shipmentStatus: status,
9 latitude: lat,
10 longitude: lon,
11 };
12 const shipmentId = driverId;
13 return this.http
14 .get<any>(
15 `${this.baseUrl}/shipments?filters[shipmentId][$eq]=${shipmentId}`
16 )
17 .pipe(
18 switchMap((response) => {
19 const shipment = response.data[0];
20 return this.http.put(`${this.baseUrl}/shipments/${shipment.id}`, {
21 data,
22 });
23 })
24 )
25 .pipe(catchError(this.handleError));
26 }
We send a PUT
request to our Strapi backend with the updated shipment data. Proper error handling is also implemented to ensure smooth operation even if something goes wrong on the server.
The post()
method is designed to send data to our Strapi backend to update the data or create a new order.
1post(endpoint: string, data: any): Observable<any> {
2 return this.http
3 .post<any>(`${this.baseUrl}${endpoint}`, data, {
4 params: { populate: '*' },
5 })
6 .pipe(catchError(this.handleError));
7}
How it works is that we pass the endpoint and data as arguments to this method. The populate: '*'
parameter ensures that we receive related data along with the main response, as explained earlier.
For authenticated requests, we need to send an authorization token. The getAuthToken()
method retrieves the stored token from our browser’s local storage. Then, the token is used to authenticate requests to protected endpoints in our Strapi backend.
1getAuthToken(): string | null {
2 return localStorage.getItem('authToken');
3}
AuthService
is responsible for user authentication in the application. It provides methods for user registration, user login, and also adding and removing authentication tokens. To make use of our AuthService, we have to utilize ApiService
to handle HTTP requests to Strapi for authentication. To achieve this, we inject the ApiService
using Angular’s inject()
function, making it immediately available for use within the AuthService
without having to inject it manually in the constructor.
1private apiService = inject(ApiService);
By injecting ApiService
, we can easily call its post()
method to send requests to our Strapi backend, allowing us to register and log in users with ease.
To register users in our application, we make use of the register()
method, which involves making a POST
request on our Strapi backend by sending the required user details like the username
, email
, and password
1register(username: string, email: string, password: string): Observable<any> {
2 return this.apiService.post('/auth/local/register', {
3 username,
4 email,
5 password,
6 });
7}
Explanation:
Here, we make a POST
request with the user's details like username
to our Strapi backend. This, in turn, creates a new user account, and stores the user details on our postgresSQL database, which can be useful during user log-in.
The login()
method authenticates our users by sending their identifier
(which is the email) and password
to Strapi’s login endpoint (/auth/local
).
1login(identifier: string, password: string): Observable<any> {
2 return this.apiService.post('/auth/local', { identifier, password });
3}
How it works is that, when the user wants to login, we make a POST
request to our backend by calling the post()
method once again from our ApiService
, passing the user’s credentials to Strapi. If the user logs in successfuly, our backend responds with a JSON Web Token (JWT), which we can then store for accessing protected resources.
Once we receive the authentication token from Strapi, we need to store it so we can include it in subsequent requests to protected endpoints. This is where saveToken()
comes in:
1saveToken(token: string) {
2 localStorage.setItem('authToken', token);
3}
This method saves the token in our browser’s local storage, giving our app access to it across different sessions. Storing the token locally allows us to maintain the user’s logged-in status, even if they close the browser and come back later.
Also, when a user logs out, we want to ensure our app removes the token so that no further requests are authenticated. The clearToken()
method does this:
1clearToken() {
2 localStorage.removeItem('authToken');
3}
It removes the token from local storage, effectively logging the user out. By clearing the token, we ensure our app prevents unauthorized access to protected endpoints, maintaining secure access for only authenticated users.
You can find the complete code on Github:
In this article series, we set up the backend of our logistics application using Strapi and PostgreSQL. We created Strapi Content Types and configured permissions to manage access effectively. On the frontend, we built the application using Angular, implementing robust authentication and authorization mechanisms.
In the next steps, we will learn how to integrate a tracking system with Leaflet in our frontend, which will help us to track our orders and also convert our logistics applicaton into a Progressive web application (PWA).
Thank you for reading!
Full stack Developer proficient in Node JS, Angular, Nest JS