Skip to content

Minimalist boilerplate for creating a Node.js application using Express.js, Passport.js and JWT with refresh token

License

Notifications You must be signed in to change notification settings

onlyjscom/express-jwt-refresh-boilerplate

Repository files navigation

Node.js - Express.js - Passport.js - JWT Refresh Token Boilerplate πŸš€

This project is a minimalist boilerplate for creating a Node.js application using Express.js, Passport.js for authentication, and JWT for stateless authentication. It includes support for JWT refresh tokens to ensure seamless user experience and enhanced security.

Table of Contents πŸ“š

Features ✨

  • Node.js: Server-side JavaScript runtime. πŸ–₯️
  • Express.js: Web framework for Node.js. 🌐
  • Passport.js: Authentication middleware for Node.js. πŸ”
  • JWT (JSON Web Tokens): Stateless authentication mechanism. πŸ›‘οΈ
  • Refresh Tokens: Mechanism to refresh JWT tokens without requiring the user to re-authenticate. πŸ”„
  • Knex.js: SQL query builder for relational databases. πŸ› οΈ
  • Cron Job: Scheduler for periodic tasks in the application. ⏲️
  • Request Validation: Ensures incoming request data adheres to expected formats using Zod. βœ”οΈ
  • SQLite: Lightweight, disk-based relational database. πŸ“¦
  • Dotenv: Loads environment variables from a .env file. 🌟

This template is ideal for beginners as it provides a simple and clean starting point without the complexity of ORM. It uses Knex.js for SQL queries to keep things straightforward and SQLite for the database to avoid the need for a separate database server.

What is Refresh Token? πŸ€”

  • A refresh token is a special kind of token that can be used to obtain a new access token after the access token has expired.
  • Refresh tokens are long-lived, and they can be used to refresh the access token without requiring the user to re-authenticate.
  • This mechanism improves the user experience by preventing the user from having to log in again after the access token expires.

Warning: ⚠️ This boilerplate does not use HTTP-only, same-site secure cookies for storing refresh and access tokens. This design choice is intentional and aligns with common practices seen in educational tutorials. Many front-end JWT implementation guides demonstrate storing tokens in local storage or session storage. However, for production environments, it's strongly recommended to use secure cookies to protect tokens from XSS attacks, especially when developing web applications.

Installation πŸ› οΈ

  1. Clone the repository:

    git clone https://github.com/onlyjscom/express-jwt-refresh-boilerplate.git
    cd express-jwt-refresh-boilerplate
  2. Install dependencies:

    npm install
  3. Create a .env file in the root directory and add the following environment variables:

    PORT=3000
    JWT_SECRET_AT=your_jwt_secret
    JWT_SECRET_RT=your_jwt_refresh_secret
    
  4. Run the database migrations:

    npm run db:migrate
  5. Start the server:

    npm start

Project Structure πŸ—‚οΈ

express-jwt-refresh-boilerplate
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ database
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”œβ”€β”€ instance.ts
β”‚   β”‚   β”œβ”€β”€ migrations
β”‚   β”‚   β”‚   β”œβ”€β”€ 20240716211511_create_users_table.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ 20240716211512_create_refresh_tokens_table.ts
β”‚   β”‚   β”‚   └── 20240716211513_create_posts_table.ts
β”‚   β”‚   β”œβ”€β”€ parsers.ts
β”‚   β”‚   └── tables.ts
β”‚   β”œβ”€β”€ index.ts
β”‚   β”œβ”€β”€ middlewares
β”‚   β”‚   β”œβ”€β”€ authenticate.ts
β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   └── validate-request.ts
β”‚   β”œβ”€β”€ modules
β”‚   β”‚   β”œβ”€β”€ auth
β”‚   β”‚   β”‚   β”œβ”€β”€ controller.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ passport.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ router.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ request-schemas.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ service.ts
β”‚   β”‚   β”‚   └── types.ts
β”‚   β”‚   β”œβ”€β”€ posts
β”‚   β”‚   β”‚   β”œβ”€β”€ controller.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ router.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ request-schemas.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ service.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types.ts
β”‚   β”‚   β”‚   └── field-validations.ts
β”‚   β”‚   └── users
β”‚   β”‚       β”œβ”€β”€ controller.ts
β”‚   β”‚       β”œβ”€β”€ index.ts
β”‚   β”‚       β”œβ”€β”€ router.ts
β”‚   β”‚       β”œβ”€β”€ request-schemas.ts
β”‚   β”‚       β”œβ”€β”€ service.ts
β”‚   β”‚       β”œβ”€β”€ types.ts
β”‚   β”‚       └── field-validations.ts
β”‚   β”œβ”€β”€ tasks
β”‚   β”‚   β”œβ”€β”€ auth.ts
β”‚   β”‚   └── index.ts
β”‚   └── utils
β”‚       β”œβ”€β”€ common-field-validations.ts
β”‚       β”œβ”€β”€ date-helpers.ts
β”‚       β”œβ”€β”€ http-errors.ts
β”‚       β”œβ”€β”€ index.ts
β”‚       └── string-helpers.ts
β”œβ”€β”€ db.sqlite
β”œβ”€β”€ knexfile.ts
β”œβ”€β”€ package.json
└── tsconfig.json

Usage πŸš€

You can import the Postman collection file in the repository root directory to test the API endpoints. Alternatively, you can use the following documentation to interact with the API.

Register

Endpoint: POST /api/auth/register

Registers a new user.

Request Body:

{
  "username": "exampleuser",
  "password": "examplePassword",
  "firstName": "John",
  "lastName": "Doe",
  "role": "user"
}

"role" field is optional. Available roles are "user" and "admin". Default role is "user" if not provided.

Response:

{
  "user": {
    "id": 1,
    "username": "exampleuser",
    "firstName": "John",
    "lastName": "Doe",
    "role": "user",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T21:15:00.000Z"
  },
  "accessToken": "jwtAccessToken",
  "refreshToken": "jwtRefreshToken"
}

Login

Endpoint: POST /api/auth/login

Logs in an existing user.

Request Body:

{
  "username": "exampleuser",
  "password": "examplePassword"
}

Response:

{
  "user": {
    "id": 1,
    "username": "exampleuser",
    "firstName": "John",
    "lastName": "Doe",
    "role": "user",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T21:15:00.000Z"
  },
  "accessToken": "jwtAccessToken",
  "refreshToken": "jwtRefreshToken"
}

Get Current User

Endpoint: GET /api/auth/me

Retrieves the current authenticated user's information.

Request Headers:

Authorization: Bearer jwtAccessToken

Response:

{
  "id": 1,
  "username": "exampleuser",
  "firstName": "John",
  "lastName": "Doe",
  "role": "user",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T21:15:00.000Z"
}

Refresh Token

Endpoint: POST /api/auth/refresh

Refreshes the JWT access token using the refresh token.

Request Headers:

Authorization: Bearer jwtRefreshToken

Response:

{
  "user": {
    "id": 1,
    "username": "exampleuser",
    "firstName": "John",
    "lastName": "Doe",
    "role": "user",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T21:15:00.000Z"
  },
  "accessToken": "newJwtAccessToken",
  "refreshToken": "newJwtRefreshToken"
}

Logout

Endpoint: POST /api/auth/logout

Logs out the current user and revokes their refresh token.

Request Headers:

Authorization: Bearer jwtRefreshToken

Response:

{
  "message": "Logged out"
}

Logout All Sessions

Endpoint: POST /api/auth/logout-all

Logs out the current user from all sessions and revokes all their refresh tokens.

Request Headers:

Authorization: Bearer jwtRefreshToken

Response:

{
  "message": "Logged out all sessions"
}

Get All Users

Endpoint: GET /api/users

Retrieves a list of all users.

Query Parameters:

  • role: Filter users by role. Available roles are "user" and "admin". (optional)

Response:

[
  {
    "id": 1,
    "username": "exampleuser",
    "firstName": "John",
    "lastName": "Doe",
    "role": "user",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T21:15:00.000Z"
  },
  {
    "id": 2,
    "username": "verissimus",
    "firstName": "Marcus",
    "lastName": "Aurelius",
    "role": "admin",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T22:00:00.000Z"
  }
]

Get User by ID

Endpoint: GET /api/users/:id

Retrieves the details of a user by their ID.

Request Parameters:

id: User's ID (integer)

Response:

{
  "id": 1,
  "username": "exampleuser",
  "firstName": "John",
  "lastName": "Doe",
  "role": "user",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T21:15:00.000Z"
}

Update User

Endpoint: PUT /api/users/:id

Updates the details of a user by their ID.

Request Parameters:

id: User's ID (integer)

Request Headers:

Authorization: Bearer jwtAccessToken

Request Body:

{
  "username": "newUsername",
  "password": "newPassword",
  "firstName": "newFirstName",
  "lastName": "newLastName",
  "role": "user"
}

All fields are optional.

Response:

{
  "id": 1,
  "username": "newusername",
  "firstName": "Newfirstname",
  "lastName": "Newlastname",
  "role": "user",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T23:35:00.000Z"
}

Delete User

Endpoint: DELETE /api/users/:id

Deletes a user by their ID.

Request Parameters:

id: User's ID (integer)

Request Headers:

Authorization: Bearer jwtAccessToken

Response:

{
  "message": "User deleted"
}

Get All Posts

Endpoint: GET /api/posts

Retrieves a list of all posts.

Query Parameters:

  • userId: Filter posts by user ID. (optional)

Response:

[
  {
    "id": 1,
    "userId": 1,
    "title": "Post Title 1",
    "content": "Post content 1",
    "createdAt": "2024-07-16T21:15:00.000Z",
    "updatedAt": "2024-07-16T21:15:00.000Z"
  },
  {
    "id": 2,
    "userId": 1,
    "title": "Post Title 2",
    "content": "Post content 2",
    "createdAt": "2024-07-16T22:00:00.000Z",
    "updatedAt": "2024-07-16T22:15:00.000Z"
  },
  {
    "id": 3,
    "userId": 2,
    "title": "Post Title 3",
    "content": "Post content 3",
    "createdAt": "2024-07-17T15:15:00.000Z",
    "updatedAt": "2024-07-17T15:15:00.000Z"
  }
]

Get a Single Post

Endpoint: GET /api/posts/:id

Retrieves a single post by its ID.

Request Parameters:

  • id: The ID of the post to retrieve.

Response:

{
  "id": 1,
  "userId": 1,
  "title": "Post Title",
  "content": "Post content",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T21:15:00.000Z"
}

Create a New Post

Endpoint: POST /api/posts

Creates a new post.

Request Headers:

Authorization: Bearer jwtAccessToken

Request Body:

{
  "title": "New Post Title",
  "content": "New Post Content"
}

Response:

{
  "id": 1,
  "userId": 1,
  "title": "New Post Title",
  "content": "New Post Content",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T21:15:00.000Z"
}

Update an Existing Post

Endpoint: PUT /api/posts/:id

Updates an existing post by its ID.

Request Headers:

Authorization: Bearer jwtAccessToken

Request Parameters:

  • id: The ID of the post to update.

Request Body:

{
  "title": "Updated Post Title",
  "content": "Updated Post Content"
}

All fields are optional.

Response:

{
  "id": 1,
  "userId": 1,
  "title": "Updated Post Title",
  "content": "Updated Post Content",
  "createdAt": "2024-07-16T21:15:00.000Z",
  "updatedAt": "2024-07-16T21:15:00.000Z"
}

Delete a Post

Endpoint: DELETE /api/posts/:id

Deletes a post by its ID.

Request Headers:

Authorization: Bearer jwtAccessToken

Request Parameters:

  • id: The ID of the post to delete.

Response:

{
  "message": "Post deleted"
}

Middlewares πŸ›‘οΈ

Authentication

To protect routes, use the authenticate middleware:

import { authenticate } from '../../middlewares';

app.get('/protected-route', authenticate, (req, res) => {
    res.json({ message: 'This is a protected route' });
});

This middleware ensures that the request is authenticated using the JWT access token. If the token is valid, the user information is attached to the request object, and the request is passed to the next middleware or route handler.

Request Validation

To validate requests, use the validateRequest middleware:

import { authenticate, validateRequest } from '../../middlewares';
import { postCreationRequestSchema } from './schemas';

app.post('/posts', [validateRequest(postCreationRequestSchema), authenticate], (req, res) => {
    // Handle post creation
});

This middleware uses Zod schemas to validate the incoming request data and ensures that only valid data is processed further.

Contributing 🀝

  1. Fork the repository.
  2. Create your feature branch: git checkout -b feature/your-feature-name.
  3. Commit your changes: git commit -m 'Add some feature'.
  4. Push to the branch: git push origin feature/your-feature-name.
  5. Open a pull request.

License πŸ“œ

This project is licensed under the MIT License. See the LICENSE file for details.

Acknowledgements πŸ™

Special thanks to the authors of the libraries and tools used in this project. Your work is greatly appreciated!