Building a To-Do List Application with Ion SST and Next.js

Osvald BernerOsvald Berner
4 min read

Lets go

In this tutorial, we'll walk through the process of creating a simple yet functional To-Do List application using Ion SST and Next.js. This project will demonstrate how to set up a serverless backend with a DynamoDB database and connect it to a Next.js frontend.

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js (v14 or later)

  • npm or yarn

  • AWS CLI configured with your credentials

Step 1: Set up the project

First, let's create a new Next.js project and add SST:

npx create-next-app@latest todo-sst
cd todo-sst
npm install sst @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Step 2: Configure SST

Create an sst.config.ts file in the root of your project:

import { SSTConfig } from "sst";
import { NextjsSite, Table } from "sst/constructs";

export default {
  config(_input) {
    return {
      name: "todo-sst",
      region: "us-east-1",
    };
  },
  stacks(app) {
    app.stack(function Site({ stack }) {
      const table = new Table(stack, "Todos", {
        fields: {
          id: "string",
          content: "string",
          completed: "boolean",
        },
        primaryIndex: { partitionKey: "id" },
      });

      const site = new NextjsSite(stack, "Site", {
        path: ".",
        environment: {
          TABLE_NAME: table.tableName,
        },
      });

      site.attachPermissions([table]);

      stack.addOutputs({
        URL: site.url,
      });
    });
  },
} satisfies SSTConfig;

This configuration sets up a DynamoDB table for our todos and a Next.js site with the necessary permissions to access the table.

Step 3: Create API routes

Create a new file pages/api/todos.ts:

import { DynamoDB } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import { NextApiRequest, NextApiResponse } from "next";
import { v4 as uuidv4 } from "uuid";

const client = new DynamoDB({});
const docClient = DynamoDBDocument.from(client);

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const { method } = req;

  switch (method) {
    case "GET":
      try {
        const result = await docClient.scan({
          TableName: process.env.TABLE_NAME,
        });
        res.status(200).json(result.Items);
      } catch (error) {
        res.status(500).json({ error: "Failed to fetch todos" });
      }
      break;

    case "POST":
      try {
        const { content } = req.body;
        const newTodo = {
          id: uuidv4(),
          content,
          completed: false,
        };
        await docClient.put({
          TableName: process.env.TABLE_NAME,
          Item: newTodo,
        });
        res.status(201).json(newTodo);
      } catch (error) {
        res.status(500).json({ error: "Failed to create todo" });
      }
      break;

    default:
      res.setHeader("Allow", ["GET", "POST"]);
      res.status(405).end(`Method ${method} Not Allowed`);
  }
}

Step 4: Create the frontend

Replace the content of pages/index.tsx with:

import { useState, useEffect } from "react";
import styles from "../styles/Home.module.css";

interface Todo {
  id: string;
  content: string;
  completed: boolean;
}

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState("");

  useEffect(() => {
    fetchTodos();
  }, []);

  const fetchTodos = async () => {
    const response = await fetch("/api/todos");
    const data = await response.json();
    setTodos(data);
  };

  const addTodo = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newTodo.trim()) return;
    const response = await fetch("/api/todos", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ content: newTodo }),
    });
    const data = await response.json();
    setTodos([...todos, data]);
    setNewTodo("");
  };

  return (
    <div className={styles.container}>
      <h1>Todo List</h1>
      <form onSubmit={addTodo}>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.content}</li>
        ))}
      </ul>
    </div>
  );
}

Step 5: Run the application locally

To run the application locally, use the following command:

npx sst dev

This will start the SST development environment and your Next.js application.

Step 6: Deploy to AWS

When you're ready to deploy your application to AWS, run:

npx sst deploy --stage prod

This command will deploy your application to AWS using the production stage.

Conclusion

In this tutorial, we've built a simple To-Do List application using Ion SST and Next.js. We've set up a serverless backend with DynamoDB, created API routes to handle CRUD operations, and built a frontend to interact with our todos.

This example demonstrates the power and simplicity of using SST to deploy serverless applications. You can further enhance this application by adding features like updating and deleting todos, user authentication, and more complex data structures.

Remember to clean up your AWS resources when you're done experimenting to avoid unnecessary charges. You can do this by running npx sst remove --stage prod.

Happy coding!

Citations: [1] https://sst.dev/blog/moving-away-from-cdk.html [2] https://ion.sst.dev/docs/ [3] https://ion.sst.dev/docs/component/aws/nextjs/ [4] https://ion.sst.dev/docs/start/aws/nextjs/ [5] https://github.com/sst/ion/pulls

0
Subscribe to my newsletter

Read articles from Osvald Berner directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Osvald Berner
Osvald Berner