DEV Community

Cover image for Server API Endpoints
Osiroski
Osiroski

Posted on

Server API Endpoints

Controllers

Models

Before you jump in and start coding the API endpoints, it's worth taking a few minutes to think about what data we need to store and the relationships between the different objects.
Our User Model will have the usual fields

  1. Username, password, email,
  2. Profile object containing { 1st, 2nd names, avatar url, bio, phone,...}
  3. passwordresetToken fields
  4. jwt Token

Defining and creating models

Models are defined using the Schema interface. The Schema allows you to define the fields stored in each document along with their validation requirements and default values.

backend/models/User.ts

import mongoose from "mongoose";
import bycrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import crypto from 'crypto';
import { model, Schema, Model, Document } from 'mongoose';

//declare point type
export interface IPoint extends Document {
    type:string;
    coordinates:string;
}
//generate point schema
const Point:Schema= new Schema({
    type: {
        type: String,
        enum: ['Point'],
        required: true
    },
    coordinates: {
        type: [Number],
        required: true
    }
});
//declare user type
export interface IUser extends Document {
    getResetPasswordToken():string;
    getSignedToken():string;
    resetPasswordToken: string|undefined;
    resetPasswordExpire: string|undefined;
    matchPassword(password: string): boolean | PromiseLike<boolean>;
    username:string;
    password:string;
    email:string;
    profile: {
        firstName: String,
        lastName: String,
        avatar: String,
        bio: String,
        phone: String,
        gender: String,
        address: {
            street1: String,
            street2: String,
            city: String,
            state: String,
            country: String,
            zip: String,
            location: {
                type: IPoint,
                required: false
            }
        },
        active:true
    }

}
// define user schema
const UserSchema: Schema = new Schema({
    username: {
        type: String,
        lowercase: true,
        unique: true,
        required: [true, "Can't be blank"],
        index: true
    },
    password: {
        type: String,
        required: true,
        select: false,
        minlength:  [8, "Please use minimum of 8 characters"],
    },
    email: {
        type: String,
        lowercase: true,
        required: [true, "Can't be blank"],
        match: [/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, 'Please use a valid address'],
        unique:true,
        index:true
    },
    profile: {
        firstName: String,
        lastName: String,
        avatar: String,
        bio: String,
        phone: String,
        gender: String,
        address: {
            street1: String,
            street2: String,
            city: String,
            state: String,
            country: String,
            zip: String,
            location: {
                type: Point,
                required: false
            }
        },
        required:false
    },
    resetPasswordToken: String,
    resetPasswordExpire: String,

    active: { type: Boolean, default: true }
});

UserSchema.pre<IUser>("save", async function (next: any) {
    if (!this.isModified('password')) {
        return next();
    }
    const salt = await bycrypt.genSalt(10);
    this.password = bycrypt.hashSync(this.password, 10);
    next();
});

UserSchema.methods.matchPassword= async function (password:string) {
    return await bycrypt.compare(password,this.password)   
}
UserSchema.methods.getSignedToken= function (password:string) {
    return jwt.sign({id:this._id},process.env.JWT_SECRET!,{
        expiresIn:process.env.JWT_EXPIRE
    })   
}
UserSchema.methods.getResetPasswordToken= function () {
    const resetToken= crypto.randomBytes(20).toString('hex');
    this.resetPasswordToken= crypto.
    createHash('sha256')
    .update(resetToken)
    .digest('hex');  
    this.resetPasswordExpire = Date.now() + 10*(60*1000) 
    return resetToken

}

export const User:Model<IUser> = model("User", UserSchema);

Enter fullscreen mode Exit fullscreen mode

Schema methods help in preforming functions on the data fields of a document. Packages such as 'bycrypt' are used to hash the passwords before storing them in the data base. NEVER STORE RAW PASSWORDS IN THE DATABASE INCASE OF DATA BREACHES WHICH HAPPENS MORE OFTEN THAN YOU THINK


Routes

Routing refers to how an application’s endpoints (URIs) respond to client requests. These routing methods specify a callback function (sometimes called “handler functions”) called when the application receives a request to the specified route (endpoint) and HTTP method. In other words, the application “listens” for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified callback function.

backend/routes/auth.ts

import express from 'express';
const router= express.Router();
//import controllers
const {register,login,forgotPassword,resetPassword}=require('../controllers/auth');

//routes
router.route('/register').post(register);
router.route('/login').post(login);
router.route('/forgotpassword').post(forgotPassword);
router.route('/resetpassword/:resetToken').post(resetPassword);

module.exports =router;
Enter fullscreen mode Exit fullscreen mode

The routes are used in backend/server.ts file.

app.use("/api/auth", require("./routes/auth"));
Enter fullscreen mode Exit fullscreen mode

The full path is appended onto "api/auth" availing 4 paths:
> api/auth/register
> api/auth/login
> api/auth/forgotpassword
> api/auth/resetpassword (takes a reset token as the parameter)


Controllers

Controller functions to get the requested data from the models, create an HTTP response and return it to the user.

backend/controllers/auth.ts

We create 4 controllers used in backend/routes/auth.ts

register

Create a new User using the model.create() function and pass the required parameters from request body

import { Response, Request } from 'express';
import {IUser, User} from '../models/User'; 

exports.register= async(req:Request,res:Response,next:any)=>{
    const {username,email,password}=req.body;
    try {
        const user:IUser= await User.create({
            username
            ,email,
            password
        });
        sendToken(user,201,res)
    } catch (error:any) {
        next(error);
    }
};
Enter fullscreen mode Exit fullscreen mode

login

import { Response, Request } from 'express';
import {ErrorResponse} from '../utils/errorResponse';
import {IUser, User} from '../models/User';
exports.login = async(req:Request,res:Response,next:any)=>{
    const {email,password}=req.body;
    if (!email || !password){
        return next(new ErrorResponse("Please provide a valid email and Password",400))
    };
    try {
        const user:IUser | null = await User.findOne({email}).select("+password");
        if (!user){
            return next(new ErrorResponse("Invalid Credentials",401))
        }
        const isMatch:boolean= await user.matchPassword(password);
        if (!isMatch){
            return next(new ErrorResponse("Invalid Credentials",401))
        }

        sendToken(user,200,res)
    } catch (error:any) {
        return next(new ErrorResponse(error.message,500))
    }
Enter fullscreen mode Exit fullscreen mode

forgotPassword and resetPassword

Here the user methods in our User model are used to generate, verify, and change the resetTokens

exports.forgotPassword=async(req:Request,res:Response,next:any)=>{
    const {email}=req.body;

    try {
        const user:IUser | null= await User.findOne({user:email});
        if (!user){
            return next(new ErrorResponse("Email could not be sent",404));
        }
        const resetToken=user.getResetPasswordToken();
        await user.save();

        const resetUrl = `http://localhost:3000/passwordreset/${resetToken}`;
        const message = `
        <h1> You have requested a password reset </h1>
        <p> Please go to this link to reset your password </p>
        <a href=${resetUrl} clicktracking=off>${resetUrl}</a> 
        `
        try {
           await sendEmail({
               to: user.email,
               text:message,
               subject:message

           }); 
           res.status(200)
           .json({
               success: true,
               data:"Email Sent"
           })
        } catch (error) {
            user.resetPasswordToken=undefined;
            user.resetPasswordExpire=undefined;
            await user.save();

            return next(new ErrorResponse("Email could not be sent", 500))

        }
    } catch (error) {
        next(error);
    }
};
exports.resetPassword=async(req:Request,res:Response,next:any)=>{
    const {password} = req.body
    const resetPasswordToken = crypto.createHash("sha256")
    .update(req.params.resetToken)
    .digest('hex');
    try {
        const user: IUser | null   = await User.findOne({
            resetPasswordToken,
            resetPasswordExpire: {$gt: Date.now(),
            }
        })

        if (!user){
            return next(new ErrorResponse("Invalid Reset Token", 400));
        }
        user.password = password;
        user.resetPasswordToken=undefined;
        user.resetPasswordExpire= undefined; 
        await user.save();
        res.status(201)
        .json({
            success: true,
            data:"Password Reset successful"
        });

    } catch (error) {
        next(error);
    }
};
Enter fullscreen mode Exit fullscreen mode

backend/utils

Contains the helper functions used in our controllers to avoid repetition

  1. errorResponse.ts
export class ErrorResponse extends Error{
    statusCode: number;
    constructor(message:any,statusCode:number){
        super(message);
        this.statusCode= statusCode;
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. emailSender.ts

Sends emails upon registration and password reset requests

import nodemailer from 'nodemailer';

interface Options {
    to: string,
    subject: string,
    text: string,

}


const sendEmail = (options: Options) => {

    const transporter = nodemailer.createTransport({
        host:process.env.EMAIL_HOST,
        port:Number(process.env.EMAIL_PORT),
        auth: {
            user:process.env.EMAIL_USER,
            pass: process.env.EMAIL_PASS
        },   
    });
    const mailOptions = {
        from: process.env.EMAIL_FROM,
        to: options.to,
        subject: options.subject,
        html: options.text

    }
    transporter.sendMail(mailOptions, function (err, info) {
        if (err) {
            console.log(err);
        } else {
            console.log(info);
        }
    })
}
module.exports = sendEmail;
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
marcomoscatelli profile image
Marco Moscatelli

Great article Osiroski!

Collapse
 
mm1120 profile image
morgan-mastrangelo

I have so many issues with your project. Please contract me if you're available.