In this article, you will learn about BookCars, a car rental platform with mobile app.
Table of Contents
- Introduction
- Prerequisites
- Quick Overview
- Frontend
- Mobile App
- Backend
- Background
- Using the Code
- API
- Frontend
- Mobile App
- Backend
- Installation
- Change Currency
- Demo Database
- Run from Code
- Run Mobile App
- Points of Interest
- History

BookCars is a car rental platform, supplier oriented, with a backend for managing car fleet and bookings, a frontend and a mobile app for car rental.
BookCars provides the following features:
- Supplier management
- Car fleet management
- Booking management
- Client management
- Multiple payment methods
- Multiple language support
- Responsive backend and frontend
- Native Mobile app for Android and iOS
In this article, you will learn how BookCars was made including a description of the main parts of the source code and the software architecture, how to deploy it, and how to run the source code. But before we dig in, we'll start with a quick overview of the platform.
- Node.js
- Express
- MongoDB
- React
- MUI
- React Native
- Expo
- JavaScript
- Git
In this section, you'll see a quick overview of the main pages of the frontend, the backend and the mobile app.
From the frontend, the user can search for available cars, choose a car and checkout.
Below is the main page of the frontend where the user can choose a pickup and drop-off locations, dates and time booking, and search for available cars.

Below is the search result of the main page where the user can choose a car for rental.

Below is the checkout page where the user can set rental options and checkout. If the user is not registered, he can checkout and register at the same time. He will receive a confirmation and activation email to set his password if he is not registered yet.

Below is the sign in page.

Below is the sign up page.

That's it! That's the main pages of the frontend.
From the mobile app, the user can search for available cars, choose a car and checkout.
The user can also receive push notifications, if the status of his booking is updated.
Below are the main pages of the mobile app where the user can choose pickup and drop-off locations, date and time booking, and search for available cars.

Below is the search result of the main page where user can choose a car for rental and checkout.

Below are sign in and sign up pages.

Below are the pages where the user can see and manage his bookings.

Below are the pages where the user can update his profile information, change his password and see his notifications.

That's it for the main pages of the mobile app.
BookCars is supplier oriented. This means that there are three types of users:
Admin
: He has full access to the backend. He can do everything. Supplier
: He has restricted access to the backend. He can only manage his cars and bookings. User
: He has access only to the frontend and the mobile app. He cannot access the backend.
BookCars is designed to work with multiple suppliers. Each supplier can manage his car fleet and bookings from the backend. BookCars can also work with only one supplier too.
From the backend, the admin user can create and manage suppliers, cars, locations, users and bookings.
When the admin user creates a new supplier, the supplier will receive an automatic email for creating his account to access the backend so he can manage his car fleet and bookings.
Below is the sign in page of the backend.

Below is the dashboard page of the backend where admins and suppliers can see and manage bookings.

Below is the page where car fleet is displayed and can be managed.

Below is the page where admins and suppliers can create new cars by providing an image and car info.

Below is the page where admins and suppliers can edit cars.

Below is the page where admins can manage platform users.

Below is the page where to edit bookings.

That's it! That's the main pages of the backend.
The basic idea behind BookCars is very simple:
- A backend: From which admins create new suppliers. Each supplier will receive an automatic email to activate his account and have access to the backend so he can manage his car fleet and bookings.
- A frontend and a mobile app: From which users can see available cars depending on some parameters such as pickup and drop-off locations, and date and time booking. Then, users can proceed to checkout in order to book their cars.
The backend, the frontend and the mobile app rely on BookCars API which is a RESTful API that exposes functions to access BookCars database.

This section describes the software architecture of BookCars including the API, the frontend, the mobile app and the backend.
BookCars API is a Node.js server application that exposes a RESTful API using Express which gives access to BookCars MongoDB database.
BookCars frontend is a React web application that is the main web interface for booking cars.
Bookcars backend is a React web application that lets admins and suppliers manage car fleets and bookings.
BookCars mobile app is a React Native application that is the main mobile app for booking cars.
BookCars API exposes all BookCars functions needed for the backend, the frontend and the mobile app. The API follows the MVC design pattern. JWT is used for authentication. There are some functions that need authentication such as functions related to managing cars and bookings and others that do not need authentication such as retrieving locations and available cars for non authenticated users.

- ./api/models/ folder contains MongoDB models.
- ./api/routes/ folder contains Express routes.
- ./api/controllers/ folder contains controllers.
- ./api/middlewares/ folder contains middlewares.
- ./api/server.js is the main server where database connection is established and routes are loaded.
- ./api/app.js is the main entry point of BookCars API.
app.js
app.js is the main entry point of BookCars API:
import app from './server.js'
import fs from 'fs'
import https from 'https'
const PORT = parseInt(process.env.BC_PORT) || 4000
const HTTPS = process.env.BC_HTTPS.toLocaleLowerCase() === 'true'
const PRIVATE_KEY = process.env.BC_PRIVATE_KEY
const CERTIFICATE = process.env.BC_CERTIFICATE
if (HTTPS) {
https.globalAgent.maxSockets = Infinity
const privateKey = fs.readFileSync(PRIVATE_KEY, 'utf8')
const certificate = fs.readFileSync(CERTIFICATE, 'utf8')
const credentials = { key: privateKey, cert: certificate }
const httpsServer = https.createServer(credentials, app)
httpsServer.listen(PORT, () => {
console.log('HTTPS server is running on Port:', PORT)
})
} else {
app.listen(PORT, () => {
console.log('HTTP server is running on Port:', PORT)
})
}
In app.js, we retrieve HTTPS
setting variable which indicate if https is enabled or not. If https is enabled, we create an https server using the provided private key and certificate and start listening. Otherwise, an http server is created and we start listening. app
is the main server where database connection is established and routes are loaded.
server.js
server.js is in the main server:
import express from 'express'
import cors from 'cors'
import mongoose from 'mongoose'
import compression from 'compression'
import helmet from 'helmet'
import nocache from 'nocache'
import strings from './config/app.config.js'
import userRoutes from './routes/userRoutes.js'
import carRoutes from './routes/carRoutes.js'
import notificationRoutes from './routes/notificationRoutes.js'
import companyRoutes from './routes/companyRoutes.js'
import locationRoutes from './routes/locationRoutes.js'
import bookingRoutes from './routes/bookingRoutes.js'
const DB_HOST = process.env.BC_DB_HOST
const DB_PORT = process.env.BC_DB_PORT
const DB_SSL = process.env.BC_DB_SSL.toLowerCase() === 'true'
const DB_SSL_KEY = process.env.BC_DB_SSL_KEY
const DB_SSL_CERT = process.env.BC_DB_SSL_CERT
const DB_SSL_CA = process.env.BC_DB_SSL_CA
const DB_DEBUG = process.env.BC_DB_DEBUG.toLowerCase() === 'true'
const DB_AUTH_SOURCE = process.env.BC_DB_AUTH_SOURCE
const DB_USERNAME = process.env.BC_DB_USERNAME
const DB_PASSWORD = process.env.BC_DB_PASSWORD
const DB_APP_NAME = process.env.BC_DB_APP_NAME
const DB_NAME = process.env.BC_DB_NAME
const DB_URI = `mongodb://${encodeURIComponent(DB_USERNAME)}:
${encodeURIComponent(DB_PASSWORD)}@${DB_HOST}:${DB_PORT}/${DB_NAME}?
authSource=${DB_AUTH_SOURCE}&appName=${DB_APP_NAME}`
let options = {}
if (DB_SSL) {
options = {
ssl: true,
sslValidate: true,
sslKey: DB_SSL_KEY,
sslCert: DB_SSL_CERT,
sslCA: [DB_SSL_CA]
}
}
mongoose.set('debug', DB_DEBUG)
mongoose.Promise = global.Promise
mongoose.connect(DB_URI, options)
.then(
() => { console.log('Database is connected') },
err => { console.error('Cannot connect to the database:', err) }
)
const app = express()
app.use(helmet.contentSecurityPolicy())
app.use(helmet.dnsPrefetchControl())
app.use(helmet.crossOriginEmbedderPolicy())
app.use(helmet.frameguard())
app.use(helmet.hidePoweredBy())
app.use(helmet.hsts())
app.use(helmet.ieNoOpen())
app.use(helmet.noSniff())
app.use(helmet.permittedCrossDomainPolicies())
app.use(helmet.referrerPolicy())
app.use(helmet.xssFilter())
app.use(helmet.originAgentCluster())
app.use(helmet.crossOriginResourcePolicy({ policy: 'cross-origin' }))
app.use(helmet.crossOriginOpenerPolicy())
app.use(nocache())
app.use(compression({ threshold: 0 }))
app.use(express.urlencoded({ limit: '50mb', extended: true }))
app.use(express.json({ limit: '50mb' }))
app.use(cors())
app.use('/', userRoutes)
app.use('/', companyRoutes)
app.use('/', locationRoutes)
app.use('/', carRoutes)
app.use('/', bookingRoutes)
app.use('/', notificationRoutes)
strings.setLanguage(process.env.BC_DEFAULT_LANGUAGE)
export default app;
First of all, we build MongoDB connection string, then we establish a connection with BookCars MongoDB database. Then we create an Express app
and load middlewares. Finally, we load Express routes and export app
.
Routes
There are six routes in BookCars API. Each route has its own controller following the MVC design pattern and SOLID principles. Below are the main routes:
userRoutes
: Provides REST functions related to users companyRoutes
: Provides REST functions related to suppliers locationRoutes
: Provides REST functions related to locations carRoutes
: Provides REST functions related to cars bookingRoutes
: Provides REST functions related to bookings notificationRoutes
: Provides REST functions related to notifications
We are not going to explain each route one by one. We'll take, for example, locationRoutes
and see how it was made:
import express from 'express'
import routeNames from '../config/locationRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as locationController from '../controllers/locationController.js'
const routes = express.Router()
routes.route(routeNames.validate).post
(authJwt.verifyToken, locationController.validate)
routes.route(routeNames.create).post(authJwt.verifyToken, locationController.create)
routes.route(routeNames.update).put(authJwt.verifyToken, locationController.update)
routes.route(routeNames.delete).delete
(authJwt.verifyToken, locationController.deleteLocation)
routes.route(routeNames.getLocation).get(locationController.getLocation)
routes.route(routeNames.getLocations).get(locationController.getLocations)
routes.route(routeNames.checkLocation).get
(authJwt.verifyToken, locationController.checkLocation)
export default routes;
First of all, we create an Express Router
. Then, we create routes using its name, its method, middlewares and its controller.
routeNames
contains locationRoutes
route names:
export default {
validate: '/api/validate-location',
create: '/api/create-location',
update: '/api/update-location/:id',
delete: '/api/delete-location/:id',
getLocation: '/api/location/:id/:language',
getLocations: '/api/locations/:page/:size/:language',
checkLocation: '/api/check-location/:id',
}
locationController
contains the main business logic regarding locations. We are not going to see all the source code of the controller since it's quite large but we'll take create
and getLocations
controller functions for example.
Below is Location
model:
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const locationSchema = new Schema({
values: {
type: [Schema.Types.ObjectId],
ref: 'LocationValue',
validate: (value) => Array.isArray(value) && value.length > 1
}
}, {
timestamps: true,
strict: true,
collection: 'Location'
})
const locationModel = mongoose.model('Location', locationSchema)
locationModel.on('index', (err) => {
if (err) {
console.error('Location index error: %s', err)
} else {
console.info('Location indexing complete')
}
})
export default locationModel;
A Location
has multiple values. One value per language. By default, English and French languages are supported.
Below is LocationValue
model:
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const locationValueSchema = new Schema({
language: {
type: String,
required: [true, "can't be blank"],
index: true,
trim: true,
lowercase: true,
minLength: 2,
maxLength: 2,
},
value: {
type: String,
required: [true, "can't be blank"],
index: true,
trim: true
}
}, {
timestamps: true,
strict: true,
collection: 'LocationValue'
})
const locationValueModel = mongoose.model('LocationValue', locationValueSchema)
locationValueModel.on('index', (err) => {
if (err) {
console.error('LocationValue index error: %s', err)
} else {
console.info('LocationValue indexing complete')
}
})
export default locationValueModel;
A LocationValue
has a language
code (ISO 639-1) and a string value
.
Below is create
controller function:
export const create = async (req, res) => {
const names = req.body
try {
const values = []
for (let i = 0; i < names.length; i++) {
const name = names[i]
const locationValue = new LocationValue({
language: name.language,
value: name.name
})
await locationValue.save()
values.push(locationValue._id)
}
const location = new Location({ values })
await location.save()
return res.sendStatus(200)
} catch (err) {
console.error(`[location.create] ${strings.DB_ERROR} ${req.body}`, err)
res.status(400).send(strings.DB_ERROR + err)
}
};
In this function, we retrieve the body of the request, we iterate through the values provided in the body (one value per language) and we create a LocationValue
. Finally, we create the location depending on the created location values.
Below is getLocations
controller function:
export const getLocations = async (req, res) => {
try {
const page = parseInt(req.params.page)
const size = parseInt(req.params.size)
const language = req.params.language
const keyword = escapeStringRegexp(req.query.s || '')
const options = 'i'
const locations = await Location.aggregate([
{
$lookup: {
from: 'LocationValue',
let: { values: '$values' },
pipeline: [
{
$match: {
$and: [
{ $expr: { $in: ['$_id', '$$values'] } },
{ $expr: { $eq: ['$language', language] } },
{ $expr: { $regexMatch: { input: '$value',
regex: keyword, options } } }
]
}
}
],
as: 'value'
}
},
{ $unwind: { path: '$value', preserveNullAndEmptyArrays: false } },
{ $addFields: { name: '$value.value' } },
{
$facet: {
resultData: [
{ $sort: { name: 1 } },
{ $skip: ((page - 1) * size) },
{ $limit: size },
],
pageInfo: [
{
$count: 'totalRecords'
}
]
}
}
], { collation: { locale: Env.DEFAULT_LANGUAGE, strength: 2 } })
res.json(locations)
} catch (err) {
console.error(`[location.getLocations]
${strings.DB_ERROR} ${req.query.s}`, err)
res.status(400).send(strings.DB_ERROR + err)
}
};
In this controller function, we retrieve locations from database using aggregate
MongoDB function and facet
to implement pagination.
Below is another simple route, notificationRoutes
:
import express from 'express'
import routeNames from '../config/notificationRoutes.config.js'
import authJwt from '../middlewares/authJwt.js'
import * as notificationController from '../controllers/notificationController.js'
const routes = express.Router()
routes.route(routeNames.notificationCounter).get
(authJwt.verifyToken, notificationController.notificationCounter)
routes.route(routeNames.notify).post
(authJwt.verifyToken, notificationController.notify)
routes.route(routeNames.getNotifications).get
(authJwt.verifyToken, notificationController.getNotifications)
routes.route(routeNames.markAsRead).post
(authJwt.verifyToken, notificationController.markAsRead)
routes.route(routeNames.markAsUnRead).post
(authJwt.verifyToken, notificationController.markAsUnRead)
routes.route(routeNames.delete).post
(authJwt.verifyToken, notificationController.deleteNotifications)
export default routes;
Below is the Notification
model:
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const notificationSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
required: [true, "can't be blank"],
ref: 'User',
index: true
},
message: {
type: String,
required: [true, "can't be blank"]
},
booking: {
type: Schema.Types.ObjectId,
ref: 'Booking'
},
isRead: {
type: Boolean,
default: false
}
}, {
timestamps: true,
strict: true,
collection: 'Notification'
})
const notificationModel = mongoose.model('Notification', notificationSchema)
notificationModel.on('index', (err) => {
if (err) {
console.error('Notification index error: %s', err)
} else {
console.info('Notification indexing complete')
}
})
export default notificationModel;
A Notification
is composed of a reference to a user
, a message
, a reference to a booking
and isRead
flag.
Below is getNotifications
controller function:
export const getNotifications = async (req, res) => {
try {
const userId = mongoose.Types.ObjectId(req.params.userId)
const page = parseInt(req.params.page)
const size = parseInt(req.params.size)
const notifications = await Notification.aggregate([
{ $match: { user: userId } },
{
$facet: {
resultData: [
{ $sort: { createdAt: -1 } },
{ $skip: ((page - 1) * size) },
{ $limit: size },
],
pageInfo: [
{
$count: 'totalRecords'
}
]
}
}
])
res.json(notifications)
} catch (err) {
console.error(strings.DB_ERROR, err)
res.status(400).send(strings.DB_ERROR + err)
}
};
In this simple controller function, we retrieve notifications using MongoDB aggregate
function, page
and size
parameters.
Below is markAsRead
controller function:
export const markAsRead = async (req, res) => {
try {
const { ids: _ids } = req.body,
ids = _ids.map(id => mongoose.Types.ObjectId(id))
const { userId: _userId } = req.params,
userId = mongoose.Types.ObjectId(_userId)
const bulk = Notification.collection.initializeOrderedBulkOp()
const notifications = await Notification.find({ _id: { $in: ids } })
bulk.find({ _id: { $in: ids },
isRead: false }).update({ $set: { isRead: true } })
bulk.execute(async (err, response) => {
if (err) {
console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
return res.status(400).send(strings.DB_ERROR + err)
}
const counter = await NotificationCounter.findOne({ user: userId })
counter.count -= notifications.filter
(notification => !notification.isRead).length
await counter.save()
return res.sendStatus(200)
})
} catch (err) {
console.error(`[notification.markAsRead] ${strings.DB_ERROR}`, err)
return res.status(400).send(strings.DB_ERROR + err)
}
};
In this controller function, we bulk update notifications and mark them as read.
The frontend is a web application built with Node.js, React and MUI. From the frontend, the user can search for available cars depending on pickup and drop-off locations, date and time booking. He can then select his booking options and finally checkout.

- ./frontend/assets/ folder contains CSS and images.
- ./frontend/pages/ folder contains React pages.
- ./frontend/components/ folder contains React components.
- ./frontend/services/ contains BookCars API client services.
- ./frontend/App.js is the main React App that contains routes.
- ./frontend/index.js is the main entry point of the frontend.
App.js is the main react App:
import React, { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
const SignIn = lazy(() => import("./components/SignIn"))
const SignUp = lazy(() => import("./components/SignUp"))
const Activate = lazy(() => import('./components/Activate'))
const ForgotPassword = lazy(() => import('./components/ForgotPassword'))
const ResetPassword = lazy(() => import('./components/ResetPassword'))
const Home = lazy(() => import("./components/Home"))
const Cars = lazy(() => import("./components/Cars"))
const CreateBooking = lazy(() => import("./components/CreateBooking"))
const Bookings = lazy(() => import("./components/Bookings"))
const Booking = lazy(() => import("./components/Booking"))
const Settings = lazy(() => import("./components/Settings"))
const Notifications = lazy(() => import("./components/Notifications"))
const ToS = lazy(() => import("./components/ToS"))
const About = lazy(() => import("./components/About"))
const ChangePassword = lazy(() => import("./components/ChangePassword"))
const Contact = lazy(() => import("./components/Contact"))
const NoMatch = lazy(() => import("./components/NoMatch"))
const App = () => {
return (
<Router>
<div className="App">
<Suspense fallback={<></>}>
<Routes>
<Route exact path="/sign-in" element={<SignIn />} />
<Route exact path="/sign-up" element={<SignUp />} />
<Route exact path='/activate' element={<Activate />} />
<Route exact path='/forgot-password' element={<ForgotPassword />} />
<Route exact path='/reset-password' element={<ResetPassword />} />
<Route exact path="/" element={<Home />} />
<Route exact path="/cars" element={<Cars />} />
<Route exact path="/create-booking" element={<CreateBooking />} />
<Route exact path="/bookings" element={<Bookings />} />
<Route exact path="/booking" element={<Booking />} />
<Route exact path="/settings" element={<Settings />} />
<Route exact path="/notifications" element={<Notifications />} />
<Route exact path="/change-password" element={<ChangePassword />} />
<Route exact path="/about" element={<About />} />
<Route exact path="/tos" element={<ToS />} />
<Route exact path="/contact" element={<Contact />} />
<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
</div>
</Router>
)
}
export default App
We are using React lazy loading to load each route.
We are not going to cover each page of the frontend, but you can open the source code and see each one if you want to.
BookCars provides a native mobile app for Android and iOS. The mobile app is built with React Native and Expo. Like for the frontend, the mobile app allows the user to search for available cars depending on pickup and drop-off locations, dates and time booking. He can then select his booking options and finally checkout.
The user receives push notifications when his booking is updated. Push notifications are built with Node.js, Expo Server SDK and Firebase.

- ./mobile/assets/ folder contains images.
- ./mobile/screens/ folder contains main pages.
- ./mobile/components/ folder contains React Native components.
- ./mobile/services/ contains BookCars API client services.
- ./mobile/App.js is the main React Native App.
- ./mobile/index.js is the main entry point of the mobile app.
index.js is the main entry point of the mobile app:
import { registerRootComponent } from 'expo'
import App from './App'
registerRootComponent(App);
App.js is the main React Native app:
import 'react-native-gesture-handler'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { RootSiblingParent } from 'react-native-root-siblings'
import { NavigationContainer } from '@react-navigation/native'
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import DrawerNavigator from './components/DrawerNavigator'
import { Provider } from 'react-native-paper'
import * as SplashScreen from 'expo-splash-screen'
import * as Notifications from 'expo-notifications'
import Helper from './common/Helper'
import NotificationService from './services/NotificationService'
import UserService from './services/UserService'
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
})
export default function App() {
const [appIsReady, setAppIsReady] = useState(false)
const responseListener = useRef()
const navigationRef = useRef()
useEffect(() => {
async function register() {
const loggedIn = await UserService.loggedIn()
if (loggedIn) {
const currentUser = await UserService.getCurrentUser()
await Helper.registerPushToken(currentUser.id)
}
}
register()
responseListener.current =
Notifications.addNotificationResponseReceivedListener(async response => {
try {
if (navigationRef.current) {
const data = response.notification.request.content.data
if (data.booking) {
if (data.user && data.notification) {
await NotificationService.markAsRead(data.user, [data.notification])
}
navigationRef.current.navigate('Booking', { id: data.booking })
} else {
navigationRef.current.navigate('Notifications')
}
}
} catch (err) {
Helper.error(err, false)
}
})
return () => {
Notifications.removeNotificationSubscription(responseListener.current)
}
}, [])
useEffect(() => {
async function prepare() {
try {
await SplashScreen.preventAutoHideAsync()
await new Promise(resolve => setTimeout(resolve, 500))
} catch (err) {
Helper.error(err)
} finally {
setAppIsReady(true)
}
}
prepare()
}, [])
const onReady = useCallback(async () => {
if (appIsReady) {
await SplashScreen.hideAsync()
}
}, [appIsReady])
if (!appIsReady) {
return null
}
return (
<SafeAreaProvider>
<Provider>
<RootSiblingParent>
<NavigationContainer ref={navigationRef} onReady={onReady}>
<ExpoStatusBar style='light' backgroundColor='rgba(0, 0, 0, .9)' />
<DrawerNavigator />
</NavigationContainer>
</RootSiblingParent>
</Provider>
</SafeAreaProvider>
)
}
We are not going to cover each screen of the mobile app, but you can open the source code and see each one if you want to.
The backend is a web application built with Node.js, React and MUI. From the backend, the admin user can create and manage suppliers, cars, locations, users and bookings. When the admin user creates a new supplier, the supplier will receive an automatic email for creating his account to access the backend so he can manage his car fleet and bookings.

- ./backend/assets/ folder contains CSS and images.
- ./backend/pages/ folder contains React pages.
- ./backend/components/ folder contains React components.
- ./backend/services/ contains BookCars API client services.
- ./backend/App.js is the main React App that contains routes.
- ./backend/index.js is the main entry point of the backend.
App.js of the backend follows similar logic like App.js of the frontend.
We are not going to cover each page of the backend but you can open the source code and see each one if you want to.
Below are the installation instructions on Ubuntu Linux.
Prerequisites
-
Install git, Node.js, NGINX, MongoDB and mongosh.
-
Configure MongoDB:
mongosh
Create admin user:
db = db.getSiblingDB('admin')
db.createUser({ user: "admin" , pwd: "PASSWORD",
roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})
Replace PASSWORD
with a strong password.
Secure MongoDB:
sudo nano /etc/mongod.conf
Change configuration as follows:
net:
port: 27017
bindIp: 0.0.0.0
security:
authorization: enabled
Restart MongoDB service:
sudo systemctl restart mongod.service
sudo systemctl status mongod.service
Instructions
- Clone BookCars repo:
cd /opt
sudo git clone https://github.com/aelassas/bookcars.git
- Add permissions:
sudo chown -R $USER:$USER /opt/bookcars
sudo chmod -R +x /opt/bookcars/__scripts
- Create deployment shortcut:
sudo ln -s /opt/bookcars/__scripts/bc-deploy.sh /usr/local/bin/bc-deploy
- Create BookCars service:
sudo cp /opt/bookcars/__services/bookcars.service /etc/systemd/system
sudo systemctl enable bookcars.service
- Add /opt/bookcars/api/.env file:
NODE_ENV = production
BC_PORT = 4002
BC_HTTPS = false
BC_PRIVATE_KEY = /etc/ssl/bookcars.com.key
BC_CERTIFICATE = /etc/ssl/bookcars.com.crt
BC_DB_HOST = 127.0.0.1
BC_DB_PORT = 27017
BC_DB_SSL = false
BC_DB_SSL_KEY = /etc/ssl/bookcars.com.key
BC_DB_SSL_CERT = /etc/ssl/bookcars.com.crt
BC_DB_SSL_CA = /etc/ssl/bookcars.com.ca.pem
BC_DB_DEBUG = false
BC_DB_APP_NAME = bookcars
BC_DB_AUTH_SOURCE = admin
BC_DB_USERNAME = admin
BC_DB_PASSWORD = PASSWORD
BC_DB_NAME = bookcars
BC_JWT_SECRET = SECRET
BC_JWT_EXPIRE_AT = 86400
BC_TOKEN_EXPIRE_AT = 86400
BC_SMTP_HOST = in-v3.mailjet.com
BC_SMTP_PORT = 587
BC_SMTP_USER = USER
BC_SMTP_PASS = PASSWORD
BC_SMTP_FROM = admin@bookcars.com
BC_ADMIN_EMAIL = admin@bookcars.com
BC_CDN_USERS = /var/www/cdn/bookcars/users
BC_CDN_TEMP_USERS = /var/www/cdn/bookcars/temp/users
BC_CDN_CARS = /var/www/cdn/bookcars/cars
BC_CDN_TEMP_CARS = /var/www/cdn/temp/bookcars/cars
BC_DEFAULT_LANGUAGE = en
BC_BACKEND_HOST = https://bookcars.com:81/
BC_FRONTEND_HOST = https://bookcars.com/
BC_MINIMUM_AGE = 21
BC_EXPO_ACCESS_TOKEN = TOKEN
You must configure the following options:
BC_SMTP_HOST
BC_DB_PASSWORD
BC_SMTP_USER
BC_SMTP_PASS
BC_SMTP_FROM
BC_ADMIN_EMAIL
BC_BACKEND_HOST
BC_FRONTEND_HOST
BC_EXPO_ACCESS_TOKEN
If you want to run the mobile app, you'll need to set up BC_EXPO_ACCESS_TOKEN by following these instructions.
If you want to enable SSL, you must configure the following options:
BC_HTTPS = true
BC_PRIVATE_KEY
BC_CERTIFICATE
- Add /opt/bookcars/backend/.env file:
PORT = 3000
REACT_APP_NODE_ENV = production
REACT_APP_BC_API_HOST = https://bookcars.com:4002
REACT_APP_BC_DEFAULT_LANGUAGE = en
REACT_APP_BC_PAGE_SIZE = 30
REACT_APP_BC_CARS_PAGE_SIZE = 15
REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
REACT_APP_BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS = https://bookcars.com/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS = https://bookcars.com/cdn/bookcars/temp/cars
REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
REACT_APP_BC_CAR_IMAGE_WIDTH = 300
REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
REACT_APP_BC_APP_TYPE = backend
REACT_APP_BC_MINIMUM_AGE = 21
You must configure the following options:
REACT_APP_BC_API_HOST
REACT_APP_BC_CDN_USERS
REACT_APP_BC_CDN_TEMP_USERS
REACT_APP_BC_CDN_CARS
REACT_APP_BC_CDN_TEMP_CARS
- Add /opt/bookcars/frontend/.env file:
PORT = 3001
REACT_APP_NODE_ENV = production
REACT_APP_BC_API_HOST = https://bookcars.com:4002
REACT_APP_BC_RECAPTCHA_SITE_KEY = GOOGLE_RECAPTCHA_SITE_KEY
REACT_APP_BC_DEFAULT_LANGUAGE = en
REACT_APP_BC_PAGE_SIZE = 30
REACT_APP_BC_CARS_PAGE_SIZE = 15
REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
REACT_APP_BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
REACT_APP_BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
REACT_APP_BC_CAR_IMAGE_WIDTH = 300
REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
REACT_APP_BC_APP_TYPE = frontend
REACT_APP_BC_MINIMUM_AGE = 21
You must configure the following options:
REACT_APP_BC_API_HOST
REACT_APP_BC_RECAPTCHA_SITE_KEY
REACT_APP_BC_CDN_USERS
REACT_APP_BC_CDN_CARS
- If you want to use the mobile app, you must add mobile/.env:
BC_API_HOST = https://bookcars.com:4002
BC_DEFAULT_LANGUAGE = en
BC_PAGE_SIZE = 20
BC_CARS_PAGE_SIZE = 8
BC_BOOKINGS_PAGE_SIZE = 8
BC_CDN_USERS = https://bookcars.com/cdn/bookcars/users
BC_CDN_CARS = https://bookcars.com/cdn/bookcars/cars
BC_COMAPANY_IMAGE_WIDTH = 60
BC_COMAPANY_IMAGE_HEIGHT = 30
BC_CAR_IMAGE_WIDTH = 300
BC_CAR_IMAGE_HEIGHT = 200
BC_APP_TYPE = frontend
BC_MINIMUM_AGE = 21
You must configure the following options:
BC_API_HOST
BC_CDN_USERS
BC_CDN_CARS
- Configure NGINX:
sudo nano /etc/nginx/sites-available/default
Change the configuration as follows for the frontend:
server {
root /var/www/bookcars/frontend;
#listen 443 http2 ssl default_server;
listen 80 default_server;
server_name _;
#ssl_certificate_key /etc/ssl/bookcars.com.key;
#ssl_certificate /etc/ssl/bookcars.com.pem;
access_log /var/log/nginx/bookcars.frontend.access.log;
error_log /var/log/nginx/bookcars.frontend.error.log;
index index.html;
location / {
try_files $uri /index.html =404;
}
location /cdn {
alias /var/www/cdn;
}
location ~ .(static)/(js|css|media)/(.+)$ {
try_files $uri $uri/ /$1/$2/$3;
}
}
If you want to enable SSL, uncomment these lines:
#listen 443 http2 ssl default_server
#ssl_certificate_key /etc/ssl/bookcars.com.key
#ssl_certificate /etc/ssl/bookcars.com.pem;
Change the configuration as follows for the backend:
server {
root /var/www/bookcars/backend;
#listen 3000 http2 ssl default_server;
listen 3000 default_server;
server_name _;
#ssl_certificate_key /etc/ssl/bookcars.com.key;
#ssl_certificate /etc/ssl/bookcars.com.pem;
#error_page 497 301 =307 https://$host:$server_port$request_uri;
access_log /var/log/nginx/bookcars.backend.access.log;
error_log /var/log/nginx/bookcars.backend.error.log;
index index.html;
location / {
try_files $uri /index.html =404;
}
location ~ .(static)/(js|css|media)/(.+)$ {
try_files $uri $uri/ /$1/$2/$3;
}
}
If you want to enable SSL, uncomment these lines:
#listen 3000 http2 ssl default_server
#ssl_certificate_key /etc/ssl/bookcars.com.key
#ssl_certificate /etc/ssl/bookcars.com.pem
#error_page 497 301 =307 https://$host:$server_port$request_uri;
Then, check nginx configuration and start nginx service:
sudo nginx -t
sudo systemctl restart nginx.service
sudo systemctl status nginx.service
- enable firewall and open BookCars ports:
sudo ufw enable
sudo ufw allow 4002/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3000/tcp
- Start bookcars service:
cd /opt/bookcars/api
npm ci
sudo systemctl start bookcars.service
Make sure that bookcars service is running with the following command:
sudo systemctl status bookcars.service
Make sure that the database connection is established by checking the logs:
tail -f /var/log/bookcars.log
- Deploy BookCars:
bc-deploy all
BookCars backend is accessible on port 3000 and the frontend is accessible on port 80 or 443 if SSL is enabled.
-
If you don't want to use the demo database, create an admin user by navigating to hostname:3000/sign-up
-
Open backend/src/App.js and comment this line to secure the backend:
<Route exact path='/sign-up' element={<Signup />} />
And run backend deployment again:
bc-deploy all
To change currency, follow the instructions below:
Frontend
- Open frontend/src/lang/common.js and change
en.CURRENCY
and fr.CURRENCY
. - Open frontend/src/lang/cars.js and change
en.CAR_CURRENCY
and fr.CAR_CURRENCY
.
Backend
Do the same thing for the backend:
- Open backend/src/lang/common.js and change
en.CURRENCY
and fr.CURRENCY
. - Open backend/src/lang/cars.js and change
en.CAR_CURRENCY
and fr.CAR_CURRENCY
.
Mobile App
Open mobile/lang/i18n.js and change en.CURRENCY
, en.CAR_CURRENCY
, fr.CURRENCY
and fr.CAR_CURRENCY
.
Download bookcars-db.zip down to your machine.
Restore BookCars demo db by using the following command:
mongorestore --verbose --drop --gzip --host=127.0.0.1
--port=27017 --username=admin --password=$PASSWORD
--authenticationDatabase=admin --nsInclude="bookcars.*" --archive=bookcars.gz
Don't forget to set $PASSWORD
.
Unzip cdn.zip on your web server so that the files will be accessible through http://localhost/cdn/bookcars/.
cdn/bookcars/ contains the following folders:
- cdn/bookcars/users: This folder contains users avatars and suppliers images.
- cdn/bookcars/cars: This folder contains cars images.
- cdn/bookcars/temp: This folder contains temporary files.
Admin user: admin@bookcars.ma
Password: B00kC4r5
Other users Password: B00kC4r5
Below are the instructions to run BookCars from code.
Prerequisites
Install git, Node.js, nginx on Linux or IIS on Windows, MongoDB and mongosh.
Configure MongoDB:
mongosh
Create admin user:
db = db.getSiblingDB('admin')
db.createUser({ user: "admin" , pwd: "PASSWORD",
roles: ["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]})
Replace PASSWORD
with a strong password.
Secure MongoDB by changing mongod.conf as follows:
net:
port: 27017
bindIp: 0.0.0.0
security:
authorization: enabled
Restart MongoDB service.
Instructions
- Download BookCars source code down to your machine.
- Add api/.env file:
NODE_ENV = development
BC_PORT = 4002
BC_HTTPS = false
BC_PRIVATE_KEY = /etc/ssl/bookcars.com.key
BC_CERTIFICATE = /etc/ssl/bookcars.com.crt
BC_DB_HOST = 127.0.0.1
BC_DB_PORT = 27017
BC_DB_SSL = false
BC_DB_SSL_KEY = /etc/ssl/bookcars.com.key
BC_DB_SSL_CERT = /etc/ssl/bookcars.com.crt
BC_DB_SSL_CA = /etc/ssl/bookcars.com.ca.pem
BC_DB_DEBUG = false
BC_DB_APP_NAME = bookcars
BC_DB_AUTH_SOURCE = admin
BC_DB_USERNAME = admin
BC_DB_PASSWORD = PASSWORD
BC_DB_NAME = bookcars
BC_JWT_SECRET = SECRET
BC_JWT_EXPIRE_AT = 86400
BC_TOKEN_EXPIRE_AT = 86400
BC_SMTP_HOST = in-v3.mailjet.com
BC_SMTP_PORT = 587
BC_SMTP_USER = USER
BC_SMTP_PASS = PASSWORD
BC_SMTP_FROM = admin@bookcars.com
BC_ADMIN_EMAIL = admin@bookcars.com
BC_CDN_USERS = /var/www/cdn/bookcars/users
BC_CDN_TEMP_USERS = /var/www/cdn/bookcars/temp/users
BC_CDN_CARS = /var/www/cdn/bookcars/cars
BC_CDN_TEMP_CARS = /var/www/cdn/bookcars/temp/cars
BC_DEFAULT_LANGUAGE = en
BC_BACKEND_HOST = http://localhost:3000/
BC_FRONTEND_HOST = http://localhost:3001/
BC_MINIMUM_AGE = 21
BC_EXPO_ACCESS_TOKEN = TOKEN
You must configure the following options:
BC_DB_PASSWORD
BC_SMTP_USER
BC_SMTP_PASS
BC_SMTP_FROM
BC_ADMIN_EMAIL
BC_BACKEND_HOST
BC_FRONTEND_HOST
BC_EXPO_ACCESS_TOKEN
Install nodemon
:
npm i -g nodemon
Run api:
cd ./api
npm install
npm run dev
- Add backend/.env file:
PORT = 3000
REACT_APP_NODE_ENV = development
REACT_APP_BC_API_HOST = http://localhost:4002
REACT_APP_BC_DEFAULT_LANGUAGE = en
REACT_APP_BC_PAGE_SIZE = 30
REACT_APP_BC_CARS_PAGE_SIZE = 15
REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
REACT_APP_BC_CDN_USERS = http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_TEMP_USERS = http://localhost/cdn/bookcars/temp/users
REACT_APP_BC_CDN_CARS = http://localhost/cdn/bookcars/cars
REACT_APP_BC_CDN_TEMP_CARS = http://localhost/cdn/bookcars/temp/cars
REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
REACT_APP_BC_CAR_IMAGE_WIDTH = 300
REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
REACT_APP_BC_APP_TYPE = backend
REACT_APP_BC_MINIMUM_AGE = 21
Run backend:
cd ./backend
npm install
npm start
- Add frontend/.env file:
PORT = 3001
REACT_APP_NODE_ENV = development
REACT_APP_BC_API_HOST = http://localhost:4002
REACT_APP_BC_RECAPTCHA_SITE_KEY = GOOGLE_RECAPTCHA_SITE_KEY
REACT_APP_BC_DEFAULT_LANGUAGE = en
REACT_APP_BC_PAGE_SIZE = 30
REACT_APP_BC_CARS_PAGE_SIZE = 15
REACT_APP_BC_BOOKINGS_PAGE_SIZE = 20
REACT_APP_BC_CDN_USERS = http://localhost/cdn/bookcars/users
REACT_APP_BC_CDN_CARS = http://localhost/cdn/bookcars/cars
REACT_APP_BC_COMAPANY_IMAGE_WIDTH = 60
REACT_APP_BC_COMAPANY_IMAGE_HEIGHT = 30
REACT_APP_BC_CAR_IMAGE_WIDTH = 300
REACT_APP_BC_CAR_IMAGE_HEIGHT = 200
REACT_APP_BC_APP_TYPE = frontend
REACT_APP_BC_MINIMUM_AGE = 21
You must configure the following option:
REACT_APP_BC_RECAPTCHA_SITE_KEY
Run frontend:
cd ./frontend
npm install
npm start
- If you want to use the mobile app, you must add mobile/.env:
BC_API_HOST = http://localhost:4002
BC_DEFAULT_LANGUAGE = en
BC_PAGE_SIZE = 20
BC_CARS_PAGE_SIZE = 8
BC_BOOKINGS_PAGE_SIZE = 8
BC_CDN_USERS = http://localhost/cdn/bookcars/users
BC_CDN_CARS = http://localhost/cdn/bookcars/cars
BC_COMAPANY_IMAGE_WIDTH = 60
BC_COMAPANY_IMAGE_HEIGHT = 30
BC_CAR_IMAGE_WIDTH = 300
BC_CAR_IMAGE_HEIGHT = 200
BC_APP_TYPE = frontend
BC_MINIMUM_AGE = 21
You must configure the following options:
BC_API_HOST
BC_CDN_USERS
BC_CDN_CARS
You need to replace localhost with your IP address.
- Configure http://localhost/cdn
- On Windows, install IIS and create C:\inetpub\wwwroot\cdn\bookcars folder.
- On Linux, install Nginx and add cdn folder by changing /etc/nginx/sites-available/default as follows:
server {
listen 80 default_server;
server_name _;
...
location /cdn {
alias /var/www/cdn;
}
}
- Create an admin user from http://localhost:3000/sign-up
- To run the mobile app, simply download Expo app on your device and run the following commands from ./mobile folder:
npm i -g expo-cli
npm install
npm start
You need to download google-services.json file and place it in ./mobile root directory for push notifications. Otherwise, the mobile app won't start. You can find detailed instructions regarding the mobile app in the next section.
Download google-services.json file and place it in ./mobile root directory for push notifications. Otherwise, the mobile app won't start. Its path can be configured from ./mobile/app.json configuration file through googleServicesFile
setting option.
To run the mobile app, create ./mobile/.env file with the following options:
BC_API_HOST = http://localhost:4002
BC_DEFAULT_LANGUAGE = en
BC_PAGE_SIZE = 20
BC_CARS_PAGE_SIZE = 8
BC_BOOKINGS_PAGE_SIZE = 8
BC_CDN_USERS = http://localhost/cdn/bookcars/users
BC_CDN_CARS = http://localhost/cdn/bookcars/cars
BC_COMAPANY_IMAGE_WIDTH = 60
BC_COMAPANY_IMAGE_HEIGHT = 30
BC_CAR_IMAGE_WIDTH = 300
BC_CAR_IMAGE_HEIGHT = 200
BC_APP_TYPE = frontend
BC_MINIMUM_AGE = 21
You must configure the following options:
BC_API_HOST
BC_CDN_USERS
BC_CDN_CARS
You need to replace localhost with your IP address.
Install demo database by following these instructions.
Configure http://localhost/cdn
- On Windows, install IIS and create C:\inetpub\wwwroot\cdn\bookcars folder.
- On Linux, install NGINX and add cdn folder by changing /etc/nginx/sites-available/default as follows:
server {
listen 80 default_server;
server_name _;
...
location /cdn {
alias /var/www/cdn;
}
}
Configure ./api by following these instructions.
Run ./api with the following command:
cd ./api
npm run dev
Run mobile app by simply downloading Expo app on your device and running the following commands from ./mobile folder:
cd ./mobile
npm i -g expo-cli
npm install
npm start
Open Expo app on your device and scan the QR code to run BookCars mobile app.
I started this project for a friend who needed a website and a mobile app for renting cars. I then decided to make the source code available and share my experience with other developers. Creating the mobile app with React Native and Expo is very straightforward. Expo makes mobile development with React Native very easy.
Using the same language (JavaScript) for both backend and frontend development is very nice and simple.
That's it! I hope you enjoyed reading this article.
- 10th November, 2022 - Initial release
- 19th November, 2022 - Updates in source code and content
- 26th November, 2022 - Added Run Mobile App instructions
- 1st December, 2022 - Updates in the backend, the frontend and the mobile app
- 4th December, 2022 - Updates in the backend, the frontend and the mobile app
- 9th December, 2022 - Updates in the backend and the frontend
- 23rd January, 2023 - Updates in installation instructions
- 31st January, 2023 - Updates in source code and instructions