Controllers

We have made schemas for our endpoints that protect us from making any obvious logical mistakes, and invalid inputs but we have not written the actual business logic that will be executed against these endpoints. We will now define controllers for each endpoint in each contract that encapsulate our logic.

Register Controller

Lets go ahead and define our first controller against the /register endpoint.

import { initServer } from "@ts-rest/express";
import { PrismaClient } from '@prisma/client';
 
const prisma = new PrismaClient();
const server = initServer();
 
const UserRouter = server.router(UserContract, {
    register : async ({ body : { name, email, password }}) => {
      const userExists = await prisma.user.findFirst({ where : { email }});
      if(userExists) return { 
        status : 400,
        body : {
          message : "User already exists"
        }
      }
      const {password : _, ...user} = await prisma.user.create({ data : { 
				name, 
				email, 
				password : encrypt(password) 
			}});    
      return {
        status : 201,
        body : user,
      }
    }
}

There are a few things worth noticing here.

  1. The body parameter of the controller is completely typed so we will get type checking, auto complete on the properties name, email, and password.
  2. We are not storing the passwords directly, but a hashed/encrypted version of it. This is a very basic, and essential security scheme.
  3. The return values in the case where the user already exists must match the one we defined in the contract(400 with error message), otherwise typescript will throw an error.
  4. The same is true for the successful registration scenario. The status code has to be 201 with the user details.

We should really appreciate how that by defining the contract beforehand, we have made it much harder for us to introduce any bugs to our codebase, as now we will have complete type-safety.

User Login Controller

The logic for the user controller is a bit longer, and it can be broken down as follows:

  1. The user provides their email and password.
  2. Check if that particular user email exists.
  3. Assuming that the user email exists, and using the same encryption method from before, compute a hash of the received password.
  4. Compare this hash with the one stored in the database.
  5. If it matches, then we will send them back an authentication token that contains an encrypted form of their user id. This is part of our authentication scheme.
import jwt from "jsonwebtoken";
 
const UserRouter = server.router(UserContract, {
    register : async ({ body }) => {/* Implemented above */},
		/* continued */
		login : async ({ body : { email, password } }) => {
      const user = await prisma.user.findFirst({ where : { email }});
 
      const IncorrectUserOrPasswordResponse = {
        status : 400,
        body : {
          message : "Username or password incorrect"
        }
      } as const;
 
      if(!user) return IncorrectUserOrPasswordResponse;
			
      if(user.password !== encrypt(password)) return IncorrectUserOrPasswordResponse;
			
			const secret = "This is the secret. keep it somewhere secure"; 
      const token = jwt.sign({ id : user.id}, secret);
      return {
        status : 200,
        body : {
          name : user.name,
          email : user.email,
          id : user.id,
          token
        }
      }
    }
}

How JsonWebTokens works

We have used the library jsonwebtoken to generate a token for us. The function jwt.sign takes in two parameters that we are concerned with.

  1. A payload is encoded within the token.
  2. A secret which is also used to generate our token.

The secret should be protected, as it is there to ensure that only we can generate these tokens, and decrypt them. One common practice is to store these secrets in a .env file server side.

The jwt.verify function will accept a token, and the same secret. If the token is valid, then it will return the serialized payload within it. We can use this in our controllers. However if the token or the secret is invalid, it will throw an error. We can consider this as unauthorized access.

We will use the jwt.verify function in a middleware, and talk more about middlewares later on in this series.

CreatePost Controller

This controller is rather straightforward in both logic, and implementation.

const s = initServer();
 
const PostRouter = s.router(PostContract, {
    createPost : async ({ body : { title, content }, req : { userId } }) => {
        /* author is the person who makes the call to begin with */
        const post = await prisma.post.create({  data : { title, content, authorId : userId } });
        return {
            status : 201,
            body : post
        }
    }
});

One interesting aspect is the request object has a userId which is being used as the authorId for the post. If you are using typescript, this will throw an error since the request object usually does not have an attribute like that. You can explicitly type the express Request object as follows:

declare global {
    namespace Express {
        export interface Request {
            userId: number;
        }
    }

Add this declaration anywhere in your code. This is only half of the problem though, as we have only typed the request to have this attribute but in reality it does not have this attribute right now. If you tried to access req.userId, it would be undefined on runtime. This is where our Middleware comes in which we will cover later. For now you can assume that the userId is indeed defined.

GetPost Controller

getPost : async ({ params : { id } }) => {
        const post = await prisma.post.findUnique({ where: { id } });
        return {
            status : 200,
            body : post ?? null
        }
    },

Notice how we have destructured the params parameter as this is what we declared in the contract. The contract really makes development much easier, and safe with its rich auto complete.


Our implementation is almost complete and now we only need our authentication middleware.